MediaWiki
REL1_21
|
00001 <?php 00039 class SwiftFileBackend extends FileBackendStore { 00041 protected $auth; // Swift authentication handler 00042 protected $authTTL; // integer seconds 00043 protected $swiftTempUrlKey; // string; shared secret value for making temp urls 00044 protected $swiftAnonUser; // string; username to handle unauthenticated requests 00045 protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled 00046 protected $swiftCDNExpiry; // integer; how long to cache things in the CDN 00047 protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled 00048 00049 // Rados Gateway specific options 00050 protected $rgwS3AccessKey; // string; S3 access key 00051 protected $rgwS3SecretKey; // string; S3 authentication key 00052 00054 protected $conn; // Swift connection handle 00055 protected $sessionStarted = 0; // integer UNIX timestamp 00056 00058 protected $connException; 00059 protected $connErrorTime = 0; // UNIX timestamp 00060 00062 protected $srvCache; 00063 00065 protected $connContainerCache; // container object cache 00066 00105 public function __construct( array $config ) { 00106 parent::__construct( $config ); 00107 if ( !MWInit::classExists( 'CF_Constants' ) ) { 00108 throw new MWException( 'SwiftCloudFiles extension not installed.' ); 00109 } 00110 // Required settings 00111 $this->auth = new CF_Authentication( 00112 $config['swiftUser'], 00113 $config['swiftKey'], 00114 null, // account; unused 00115 $config['swiftAuthUrl'] 00116 ); 00117 // Optional settings 00118 $this->authTTL = isset( $config['swiftAuthTTL'] ) 00119 ? $config['swiftAuthTTL'] 00120 : 5 * 60; // some sane number 00121 $this->swiftAnonUser = isset( $config['swiftAnonUser'] ) 00122 ? $config['swiftAnonUser'] 00123 : ''; 00124 $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] ) 00125 ? $config['swiftTempUrlKey'] 00126 : ''; 00127 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] ) 00128 ? $config['shardViaHashLevels'] 00129 : ''; 00130 $this->swiftUseCDN = isset( $config['swiftUseCDN'] ) 00131 ? $config['swiftUseCDN'] 00132 : false; 00133 $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] ) 00134 ? $config['swiftCDNExpiry'] 00135 : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org) 00136 $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] ) 00137 ? $config['swiftCDNPurgable'] 00138 : true; 00139 $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] ) 00140 ? $config['rgwS3AccessKey'] 00141 : ''; 00142 $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] ) 00143 ? $config['rgwS3SecretKey'] 00144 : ''; 00145 // Cache container information to mask latency 00146 $this->memCache = wfGetMainCache(); 00147 // Process cache for container info 00148 $this->connContainerCache = new ProcessCacheLRU( 300 ); 00149 // Cache auth token information to avoid RTTs 00150 if ( !empty( $config['cacheAuthInfo'] ) ) { 00151 if ( PHP_SAPI === 'cli' ) { 00152 $this->srvCache = wfGetMainCache(); // preferrably memcached 00153 } else { 00154 try { // look for APC, XCache, WinCache, ect... 00155 $this->srvCache = ObjectCache::newAccelerator( array() ); 00156 } catch ( Exception $e ) {} 00157 } 00158 } 00159 $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff(); 00160 } 00161 00166 protected function resolveContainerPath( $container, $relStoragePath ) { 00167 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF 00168 return null; // not UTF-8, makes it hard to use CF and the swift HTTP API 00169 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) { 00170 return null; // too long for Swift 00171 } 00172 return $relStoragePath; 00173 } 00174 00179 public function isPathUsableInternal( $storagePath ) { 00180 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath ); 00181 if ( $rel === null ) { 00182 return false; // invalid 00183 } 00184 00185 try { 00186 $this->getContainer( $container ); 00187 return true; // container exists 00188 } catch ( NoSuchContainerException $e ) { 00189 } catch ( CloudFilesException $e ) { // some other exception? 00190 $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) ); 00191 } 00192 00193 return false; 00194 } 00195 00200 protected function truncDisp( $disposition ) { 00201 $res = ''; 00202 foreach ( explode( ';', $disposition ) as $part ) { 00203 $part = trim( $part ); 00204 $new = ( $res === '' ) ? $part : "{$res};{$part}"; 00205 if ( strlen( $new ) <= 255 ) { 00206 $res = $new; 00207 } else { 00208 break; // too long; sigh 00209 } 00210 } 00211 return $res; 00212 } 00213 00218 protected function doCreateInternal( array $params ) { 00219 $status = Status::newGood(); 00220 00221 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); 00222 if ( $dstRel === null ) { 00223 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 00224 return $status; 00225 } 00226 00227 // (a) Check the destination container and object 00228 try { 00229 $dContObj = $this->getContainer( $dstCont ); 00230 } catch ( NoSuchContainerException $e ) { 00231 $status->fatal( 'backend-fail-create', $params['dst'] ); 00232 return $status; 00233 } catch ( CloudFilesException $e ) { // some other exception? 00234 $this->handleException( $e, $status, __METHOD__, $params ); 00235 return $status; 00236 } 00237 00238 // (b) Get a SHA-1 hash of the object 00239 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 ); 00240 00241 // (c) Actually create the object 00242 try { 00243 // Create a fresh CF_Object with no fields preloaded. 00244 // We don't want to preserve headers, metadata, and such. 00245 $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD 00246 $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) ); 00247 // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59). 00248 // The MD5 here will be checked within Swift against its own MD5. 00249 $obj->set_etag( md5( $params['content'] ) ); 00250 // Use the same content type as StreamFile for security 00251 $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); 00252 if ( !strlen( $obj->content_type ) ) { // special case 00253 $obj->content_type = 'unknown/unknown'; 00254 } 00255 // Set the Content-Disposition header if requested 00256 if ( isset( $params['disposition'] ) ) { 00257 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); 00258 } 00259 // Set any other custom headers if requested 00260 if ( isset( $params['headers'] ) ) { 00261 $obj->headers += $params['headers']; 00262 } 00263 if ( !empty( $params['async'] ) ) { // deferred 00264 $op = $obj->write_async( $params['content'] ); 00265 $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op ); 00266 $status->value->affectedObjects[] = $obj; 00267 } else { // actually write the object in Swift 00268 $obj->write( $params['content'] ); 00269 $this->purgeCDNCache( array( $obj ) ); 00270 } 00271 } catch ( CDNNotEnabledException $e ) { 00272 // CDN not enabled; nothing to see here 00273 } catch ( BadContentTypeException $e ) { 00274 $status->fatal( 'backend-fail-contenttype', $params['dst'] ); 00275 } catch ( CloudFilesException $e ) { // some other exception? 00276 $this->handleException( $e, $status, __METHOD__, $params ); 00277 } 00278 00279 return $status; 00280 } 00281 00285 protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) { 00286 try { 00287 $cfOp->getLastResponse(); 00288 } catch ( BadContentTypeException $e ) { 00289 $status->fatal( 'backend-fail-contenttype', $params['dst'] ); 00290 } 00291 } 00292 00297 protected function doStoreInternal( array $params ) { 00298 $status = Status::newGood(); 00299 00300 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); 00301 if ( $dstRel === null ) { 00302 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 00303 return $status; 00304 } 00305 00306 // (a) Check the destination container and object 00307 try { 00308 $dContObj = $this->getContainer( $dstCont ); 00309 } catch ( NoSuchContainerException $e ) { 00310 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00311 return $status; 00312 } catch ( CloudFilesException $e ) { // some other exception? 00313 $this->handleException( $e, $status, __METHOD__, $params ); 00314 return $status; 00315 } 00316 00317 // (b) Get a SHA-1 hash of the object 00318 wfSuppressWarnings(); 00319 $sha1Hash = sha1_file( $params['src'] ); 00320 wfRestoreWarnings(); 00321 if ( $sha1Hash === false ) { // source doesn't exist? 00322 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00323 return $status; 00324 } 00325 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 ); 00326 00327 // (c) Actually store the object 00328 try { 00329 // Create a fresh CF_Object with no fields preloaded. 00330 // We don't want to preserve headers, metadata, and such. 00331 $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD 00332 $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) ); 00333 // The MD5 here will be checked within Swift against its own MD5. 00334 $obj->set_etag( md5_file( $params['src'] ) ); 00335 // Use the same content type as StreamFile for security 00336 $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] ); 00337 if ( !strlen( $obj->content_type ) ) { // special case 00338 $obj->content_type = 'unknown/unknown'; 00339 } 00340 // Set the Content-Disposition header if requested 00341 if ( isset( $params['disposition'] ) ) { 00342 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); 00343 } 00344 // Set any other custom headers if requested 00345 if ( isset( $params['headers'] ) ) { 00346 $obj->headers += $params['headers']; 00347 } 00348 if ( !empty( $params['async'] ) ) { // deferred 00349 wfSuppressWarnings(); 00350 $fp = fopen( $params['src'], 'rb' ); 00351 wfRestoreWarnings(); 00352 if ( !$fp ) { 00353 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00354 } else { 00355 $op = $obj->write_async( $fp, filesize( $params['src'] ), true ); 00356 $status->value = new SwiftFileOpHandle( $this, $params, 'Store', $op ); 00357 $status->value->resourcesToClose[] = $fp; 00358 $status->value->affectedObjects[] = $obj; 00359 } 00360 } else { // actually write the object in Swift 00361 $obj->load_from_filename( $params['src'], true ); // calls $obj->write() 00362 $this->purgeCDNCache( array( $obj ) ); 00363 } 00364 } catch ( CDNNotEnabledException $e ) { 00365 // CDN not enabled; nothing to see here 00366 } catch ( BadContentTypeException $e ) { 00367 $status->fatal( 'backend-fail-contenttype', $params['dst'] ); 00368 } catch ( IOException $e ) { 00369 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00370 } catch ( CloudFilesException $e ) { // some other exception? 00371 $this->handleException( $e, $status, __METHOD__, $params ); 00372 } 00373 00374 return $status; 00375 } 00376 00380 protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) { 00381 try { 00382 $cfOp->getLastResponse(); 00383 } catch ( BadContentTypeException $e ) { 00384 $status->fatal( 'backend-fail-contenttype', $params['dst'] ); 00385 } catch ( IOException $e ) { 00386 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00387 } 00388 } 00389 00394 protected function doCopyInternal( array $params ) { 00395 $status = Status::newGood(); 00396 00397 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 00398 if ( $srcRel === null ) { 00399 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 00400 return $status; 00401 } 00402 00403 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); 00404 if ( $dstRel === null ) { 00405 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 00406 return $status; 00407 } 00408 00409 // (a) Check the source/destination containers and destination object 00410 try { 00411 $sContObj = $this->getContainer( $srcCont ); 00412 $dContObj = $this->getContainer( $dstCont ); 00413 } catch ( NoSuchContainerException $e ) { 00414 if ( empty( $params['ignoreMissingSource'] ) || isset( $sContObj ) ) { 00415 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00416 } 00417 return $status; 00418 } catch ( CloudFilesException $e ) { // some other exception? 00419 $this->handleException( $e, $status, __METHOD__, $params ); 00420 return $status; 00421 } 00422 00423 // (b) Actually copy the file to the destination 00424 try { 00425 $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD 00426 $hdrs = array(); // source file headers to override with new values 00427 if ( isset( $params['disposition'] ) ) { 00428 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); 00429 } 00430 if ( !empty( $params['async'] ) ) { // deferred 00431 $op = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs ); 00432 $status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $op ); 00433 $status->value->affectedObjects[] = $dstObj; 00434 } else { // actually write the object in Swift 00435 $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs ); 00436 $this->purgeCDNCache( array( $dstObj ) ); 00437 } 00438 } catch ( CDNNotEnabledException $e ) { 00439 // CDN not enabled; nothing to see here 00440 } catch ( NoSuchObjectException $e ) { // source object does not exist 00441 if ( empty( $params['ignoreMissingSource'] ) ) { 00442 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00443 } 00444 } catch ( CloudFilesException $e ) { // some other exception? 00445 $this->handleException( $e, $status, __METHOD__, $params ); 00446 } 00447 00448 return $status; 00449 } 00450 00454 protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) { 00455 try { 00456 $cfOp->getLastResponse(); 00457 } catch ( NoSuchObjectException $e ) { // source object does not exist 00458 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] ); 00459 } 00460 } 00461 00466 protected function doMoveInternal( array $params ) { 00467 $status = Status::newGood(); 00468 00469 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 00470 if ( $srcRel === null ) { 00471 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 00472 return $status; 00473 } 00474 00475 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] ); 00476 if ( $dstRel === null ) { 00477 $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); 00478 return $status; 00479 } 00480 00481 // (a) Check the source/destination containers and destination object 00482 try { 00483 $sContObj = $this->getContainer( $srcCont ); 00484 $dContObj = $this->getContainer( $dstCont ); 00485 } catch ( NoSuchContainerException $e ) { 00486 if ( empty( $params['ignoreMissingSource'] ) || isset( $sContObj ) ) { 00487 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); 00488 } 00489 return $status; 00490 } catch ( CloudFilesException $e ) { // some other exception? 00491 $this->handleException( $e, $status, __METHOD__, $params ); 00492 return $status; 00493 } 00494 00495 // (b) Actually move the file to the destination 00496 try { 00497 $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD 00498 $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD 00499 $hdrs = array(); // source file headers to override with new values 00500 if ( isset( $params['disposition'] ) ) { 00501 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); 00502 } 00503 if ( !empty( $params['async'] ) ) { // deferred 00504 $op = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs ); 00505 $status->value = new SwiftFileOpHandle( $this, $params, 'Move', $op ); 00506 $status->value->affectedObjects[] = $srcObj; 00507 $status->value->affectedObjects[] = $dstObj; 00508 } else { // actually write the object in Swift 00509 $sContObj->move_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs ); 00510 $this->purgeCDNCache( array( $srcObj ) ); 00511 $this->purgeCDNCache( array( $dstObj ) ); 00512 } 00513 } catch ( CDNNotEnabledException $e ) { 00514 // CDN not enabled; nothing to see here 00515 } catch ( NoSuchObjectException $e ) { // source object does not exist 00516 if ( empty( $params['ignoreMissingSource'] ) ) { 00517 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); 00518 } 00519 } catch ( CloudFilesException $e ) { // some other exception? 00520 $this->handleException( $e, $status, __METHOD__, $params ); 00521 } 00522 00523 return $status; 00524 } 00525 00529 protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) { 00530 try { 00531 $cfOp->getLastResponse(); 00532 } catch ( NoSuchObjectException $e ) { // source object does not exist 00533 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] ); 00534 } 00535 } 00536 00541 protected function doDeleteInternal( array $params ) { 00542 $status = Status::newGood(); 00543 00544 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 00545 if ( $srcRel === null ) { 00546 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 00547 return $status; 00548 } 00549 00550 try { 00551 $sContObj = $this->getContainer( $srcCont ); 00552 $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD 00553 if ( !empty( $params['async'] ) ) { // deferred 00554 $op = $sContObj->delete_object_async( $srcRel ); 00555 $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $op ); 00556 $status->value->affectedObjects[] = $srcObj; 00557 } else { // actually write the object in Swift 00558 $sContObj->delete_object( $srcRel ); 00559 $this->purgeCDNCache( array( $srcObj ) ); 00560 } 00561 } catch ( CDNNotEnabledException $e ) { 00562 // CDN not enabled; nothing to see here 00563 } catch ( NoSuchContainerException $e ) { 00564 if ( empty( $params['ignoreMissingSource'] ) ) { 00565 $status->fatal( 'backend-fail-delete', $params['src'] ); 00566 } 00567 } catch ( NoSuchObjectException $e ) { 00568 if ( empty( $params['ignoreMissingSource'] ) ) { 00569 $status->fatal( 'backend-fail-delete', $params['src'] ); 00570 } 00571 } catch ( CloudFilesException $e ) { // some other exception? 00572 $this->handleException( $e, $status, __METHOD__, $params ); 00573 } 00574 00575 return $status; 00576 } 00577 00581 protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) { 00582 try { 00583 $cfOp->getLastResponse(); 00584 } catch ( NoSuchContainerException $e ) { 00585 $status->fatal( 'backend-fail-delete', $params['src'] ); 00586 } catch ( NoSuchObjectException $e ) { 00587 if ( empty( $params['ignoreMissingSource'] ) ) { 00588 $status->fatal( 'backend-fail-delete', $params['src'] ); 00589 } 00590 } 00591 } 00592 00597 protected function doDescribeInternal( array $params ) { 00598 $status = Status::newGood(); 00599 00600 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 00601 if ( $srcRel === null ) { 00602 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 00603 return $status; 00604 } 00605 00606 $hdrs = isset( $params['headers'] ) ? $params['headers'] : array(); 00607 // Set the Content-Disposition header if requested 00608 if ( isset( $params['disposition'] ) ) { 00609 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] ); 00610 } 00611 00612 try { 00613 $sContObj = $this->getContainer( $srcCont ); 00614 // Get the latest version of the current metadata 00615 $srcObj = $sContObj->get_object( $srcRel, 00616 $this->headersFromParams( array( 'latest' => true ) ) ); 00617 // Merge in the metadata updates... 00618 $srcObj->headers = $hdrs + $srcObj->headers; 00619 $srcObj->sync_metadata(); // save to Swift 00620 $this->purgeCDNCache( array( $srcObj ) ); 00621 } catch ( CDNNotEnabledException $e ) { 00622 // CDN not enabled; nothing to see here 00623 } catch ( NoSuchContainerException $e ) { 00624 $status->fatal( 'backend-fail-describe', $params['src'] ); 00625 } catch ( NoSuchObjectException $e ) { 00626 $status->fatal( 'backend-fail-describe', $params['src'] ); 00627 } catch ( CloudFilesException $e ) { // some other exception? 00628 $this->handleException( $e, $status, __METHOD__, $params ); 00629 } 00630 00631 return $status; 00632 } 00633 00638 protected function doPrepareInternal( $fullCont, $dir, array $params ) { 00639 $status = Status::newGood(); 00640 00641 // (a) Check if container already exists 00642 try { 00643 $this->getContainer( $fullCont ); 00644 // NoSuchContainerException not thrown: container must exist 00645 return $status; // already exists 00646 } catch ( NoSuchContainerException $e ) { 00647 // NoSuchContainerException thrown: container does not exist 00648 } catch ( CloudFilesException $e ) { // some other exception? 00649 $this->handleException( $e, $status, __METHOD__, $params ); 00650 return $status; 00651 } 00652 00653 // (b) Create container as needed 00654 try { 00655 $contObj = $this->createContainer( $fullCont ); 00656 if ( !empty( $params['noAccess'] ) ) { 00657 // Make container private to end-users... 00658 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); 00659 } else { 00660 // Make container public to end-users... 00661 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) ); 00662 } 00663 if ( $this->swiftUseCDN ) { // Rackspace style CDN 00664 $contObj->make_public( $this->swiftCDNExpiry ); 00665 } 00666 } catch ( CDNNotEnabledException $e ) { 00667 // CDN not enabled; nothing to see here 00668 } catch ( CloudFilesException $e ) { // some other exception? 00669 $this->handleException( $e, $status, __METHOD__, $params ); 00670 return $status; 00671 } 00672 00673 return $status; 00674 } 00675 00680 protected function doSecureInternal( $fullCont, $dir, array $params ) { 00681 $status = Status::newGood(); 00682 if ( empty( $params['noAccess'] ) ) { 00683 return $status; // nothing to do 00684 } 00685 00686 // Restrict container from end-users... 00687 try { 00688 // doPrepareInternal() should have been called, 00689 // so the Swift container should already exist... 00690 $contObj = $this->getContainer( $fullCont ); // normally a cache hit 00691 // NoSuchContainerException not thrown: container must exist 00692 00693 // Make container private to end-users... 00694 $status->merge( $this->setContainerAccess( 00695 $contObj, 00696 array( $this->auth->username ), // read 00697 array( $this->auth->username ) // write 00698 ) ); 00699 if ( $this->swiftUseCDN && $contObj->is_public() ) { // Rackspace style CDN 00700 $contObj->make_private(); 00701 } 00702 } catch ( CDNNotEnabledException $e ) { 00703 // CDN not enabled; nothing to see here 00704 } catch ( CloudFilesException $e ) { // some other exception? 00705 $this->handleException( $e, $status, __METHOD__, $params ); 00706 } 00707 00708 return $status; 00709 } 00710 00715 protected function doPublishInternal( $fullCont, $dir, array $params ) { 00716 $status = Status::newGood(); 00717 00718 // Unrestrict container from end-users... 00719 try { 00720 // doPrepareInternal() should have been called, 00721 // so the Swift container should already exist... 00722 $contObj = $this->getContainer( $fullCont ); // normally a cache hit 00723 // NoSuchContainerException not thrown: container must exist 00724 00725 // Make container public to end-users... 00726 if ( $this->swiftAnonUser != '' ) { 00727 $status->merge( $this->setContainerAccess( 00728 $contObj, 00729 array( $this->auth->username, $this->swiftAnonUser ), // read 00730 array( $this->auth->username, $this->swiftAnonUser ) // write 00731 ) ); 00732 } else { 00733 $status->merge( $this->setContainerAccess( 00734 $contObj, 00735 array( $this->auth->username, '.r:*' ), // read 00736 array( $this->auth->username ) // write 00737 ) ); 00738 } 00739 if ( $this->swiftUseCDN && !$contObj->is_public() ) { // Rackspace style CDN 00740 $contObj->make_public(); 00741 } 00742 } catch ( CDNNotEnabledException $e ) { 00743 // CDN not enabled; nothing to see here 00744 } catch ( CloudFilesException $e ) { // some other exception? 00745 $this->handleException( $e, $status, __METHOD__, $params ); 00746 } 00747 00748 return $status; 00749 } 00750 00755 protected function doCleanInternal( $fullCont, $dir, array $params ) { 00756 $status = Status::newGood(); 00757 00758 // Only containers themselves can be removed, all else is virtual 00759 if ( $dir != '' ) { 00760 return $status; // nothing to do 00761 } 00762 00763 // (a) Check the container 00764 try { 00765 $contObj = $this->getContainer( $fullCont, true ); 00766 } catch ( NoSuchContainerException $e ) { 00767 return $status; // ok, nothing to do 00768 } catch ( CloudFilesException $e ) { // some other exception? 00769 $this->handleException( $e, $status, __METHOD__, $params ); 00770 return $status; 00771 } 00772 00773 // (b) Delete the container if empty 00774 if ( $contObj->object_count == 0 ) { 00775 try { 00776 $this->deleteContainer( $fullCont ); 00777 } catch ( NoSuchContainerException $e ) { 00778 return $status; // race? 00779 } catch ( NonEmptyContainerException $e ) { 00780 return $status; // race? consistency delay? 00781 } catch ( CloudFilesException $e ) { // some other exception? 00782 $this->handleException( $e, $status, __METHOD__, $params ); 00783 return $status; 00784 } 00785 } 00786 00787 return $status; 00788 } 00789 00794 protected function doGetFileStat( array $params ) { 00795 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 00796 if ( $srcRel === null ) { 00797 return false; // invalid storage path 00798 } 00799 00800 $stat = false; 00801 try { 00802 $contObj = $this->getContainer( $srcCont ); 00803 $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) ); 00804 $this->addMissingMetadata( $srcObj, $params['src'] ); 00805 $stat = array( 00806 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW 00807 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ), 00808 'size' => (int)$srcObj->content_length, 00809 'sha1' => $srcObj->getMetadataValue( 'Sha1base36' ) 00810 ); 00811 } catch ( NoSuchContainerException $e ) { 00812 } catch ( NoSuchObjectException $e ) { 00813 } catch ( CloudFilesException $e ) { // some other exception? 00814 $stat = null; 00815 $this->handleException( $e, null, __METHOD__, $params ); 00816 } 00817 00818 return $stat; 00819 } 00820 00829 protected function addMissingMetadata( CF_Object $obj, $path ) { 00830 if ( $obj->getMetadataValue( 'Sha1base36' ) !== null ) { 00831 return true; // nothing to do 00832 } 00833 wfProfileIn( __METHOD__ ); 00834 trigger_error( "$path was not stored with SHA-1 metadata.", E_USER_WARNING ); 00835 $status = Status::newGood(); 00836 $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status ); 00837 if ( $status->isOK() ) { 00838 $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) ); 00839 if ( $tmpFile ) { 00840 $hash = $tmpFile->getSha1Base36(); 00841 if ( $hash !== false ) { 00842 $obj->setMetadataValues( array( 'Sha1base36' => $hash ) ); 00843 $obj->sync_metadata(); // save to Swift 00844 wfProfileOut( __METHOD__ ); 00845 return true; // success 00846 } 00847 } 00848 } 00849 trigger_error( "Unable to set SHA-1 metadata for $path", E_USER_WARNING ); 00850 $obj->setMetadataValues( array( 'Sha1base36' => false ) ); 00851 wfProfileOut( __METHOD__ ); 00852 return false; // failed 00853 } 00854 00859 protected function doGetFileContentsMulti( array $params ) { 00860 $contents = array(); 00861 00862 $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging 00863 // Blindly create tmp files and stream to them, catching any exception if the file does 00864 // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata(). 00865 foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) { 00866 $cfOps = array(); // (path => CF_Async_Op) 00867 00868 foreach ( $pathBatch as $path ) { // each path in this concurrent batch 00869 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); 00870 if ( $srcRel === null ) { 00871 $contents[$path] = false; 00872 continue; 00873 } 00874 $data = false; 00875 try { 00876 $sContObj = $this->getContainer( $srcCont ); 00877 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD 00878 // Create a new temporary memory file... 00879 $handle = fopen( 'php://temp', 'wb' ); 00880 if ( $handle ) { 00881 $headers = $this->headersFromParams( $params ); 00882 if ( count( $pathBatch ) > 1 ) { 00883 $cfOps[$path] = $obj->stream_async( $handle, $headers ); 00884 $cfOps[$path]->_file_handle = $handle; // close this later 00885 } else { 00886 $obj->stream( $handle, $headers ); 00887 rewind( $handle ); // start from the beginning 00888 $data = stream_get_contents( $handle ); 00889 fclose( $handle ); 00890 } 00891 } else { 00892 $data = false; 00893 } 00894 } catch ( NoSuchContainerException $e ) { 00895 $data = false; 00896 } catch ( NoSuchObjectException $e ) { 00897 $data = false; 00898 } catch ( CloudFilesException $e ) { // some other exception? 00899 $data = false; 00900 $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep ); 00901 } 00902 $contents[$path] = $data; 00903 } 00904 00905 $batch = new CF_Async_Op_Batch( $cfOps ); 00906 $cfOps = $batch->execute(); 00907 foreach ( $cfOps as $path => $cfOp ) { 00908 try { 00909 $cfOp->getLastResponse(); 00910 rewind( $cfOp->_file_handle ); // start from the beginning 00911 $contents[$path] = stream_get_contents( $cfOp->_file_handle ); 00912 } catch ( NoSuchContainerException $e ) { 00913 $contents[$path] = false; 00914 } catch ( NoSuchObjectException $e ) { 00915 $contents[$path] = false; 00916 } catch ( CloudFilesException $e ) { // some other exception? 00917 $contents[$path] = false; 00918 $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep ); 00919 } 00920 fclose( $cfOp->_file_handle ); // close open handle 00921 } 00922 } 00923 00924 return $contents; 00925 } 00926 00931 protected function doDirectoryExists( $fullCont, $dir, array $params ) { 00932 try { 00933 $container = $this->getContainer( $fullCont ); 00934 $prefix = ( $dir == '' ) ? null : "{$dir}/"; 00935 return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 ); 00936 } catch ( NoSuchContainerException $e ) { 00937 return false; 00938 } catch ( CloudFilesException $e ) { // some other exception? 00939 $this->handleException( $e, null, __METHOD__, 00940 array( 'cont' => $fullCont, 'dir' => $dir ) ); 00941 } 00942 00943 return null; // error 00944 } 00945 00950 public function getDirectoryListInternal( $fullCont, $dir, array $params ) { 00951 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params ); 00952 } 00953 00958 public function getFileListInternal( $fullCont, $dir, array $params ) { 00959 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params ); 00960 } 00961 00972 public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) { 00973 $dirs = array(); 00974 if ( $after === INF ) { 00975 return $dirs; // nothing more 00976 } 00977 wfProfileIn( __METHOD__ . '-' . $this->name ); 00978 00979 try { 00980 $container = $this->getContainer( $fullCont ); 00981 $prefix = ( $dir == '' ) ? null : "{$dir}/"; 00982 // Non-recursive: only list dirs right under $dir 00983 if ( !empty( $params['topOnly'] ) ) { 00984 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' ); 00985 foreach ( $objects as $object ) { // files and dirs 00986 if ( substr( $object, -1 ) === '/' ) { 00987 $dirs[] = $object; // directories end in '/' 00988 } 00989 } 00990 // Recursive: list all dirs under $dir and its subdirs 00991 } else { 00992 // Get directory from last item of prior page 00993 $lastDir = $this->getParentDir( $after ); // must be first page 00994 $objects = $container->list_objects( $limit, $after, $prefix ); 00995 foreach ( $objects as $object ) { // files 00996 $objectDir = $this->getParentDir( $object ); // directory of object 00997 if ( $objectDir !== false && $objectDir !== $dir ) { 00998 // Swift stores paths in UTF-8, using binary sorting. 00999 // See function "create_container_table" in common/db.py. 01000 // If a directory is not "greater" than the last one, 01001 // then it was already listed by the calling iterator. 01002 if ( strcmp( $objectDir, $lastDir ) > 0 ) { 01003 $pDir = $objectDir; 01004 do { // add dir and all its parent dirs 01005 $dirs[] = "{$pDir}/"; 01006 $pDir = $this->getParentDir( $pDir ); 01007 } while ( $pDir !== false // sanity 01008 && strcmp( $pDir, $lastDir ) > 0 // not done already 01009 && strlen( $pDir ) > strlen( $dir ) // within $dir 01010 ); 01011 } 01012 $lastDir = $objectDir; 01013 } 01014 } 01015 } 01016 if ( count( $objects ) < $limit ) { 01017 $after = INF; // avoid a second RTT 01018 } else { 01019 $after = end( $objects ); // update last item 01020 } 01021 } catch ( NoSuchContainerException $e ) { 01022 } catch ( CloudFilesException $e ) { // some other exception? 01023 $this->handleException( $e, null, __METHOD__, 01024 array( 'cont' => $fullCont, 'dir' => $dir ) ); 01025 } 01026 01027 wfProfileOut( __METHOD__ . '-' . $this->name ); 01028 return $dirs; 01029 } 01030 01031 protected function getParentDir( $path ) { 01032 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false; 01033 } 01034 01045 public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) { 01046 $files = array(); 01047 if ( $after === INF ) { 01048 return $files; // nothing more 01049 } 01050 wfProfileIn( __METHOD__ . '-' . $this->name ); 01051 01052 try { 01053 $container = $this->getContainer( $fullCont ); 01054 $prefix = ( $dir == '' ) ? null : "{$dir}/"; 01055 // Non-recursive: only list files right under $dir 01056 if ( !empty( $params['topOnly'] ) ) { // files and dirs 01057 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' ); 01058 foreach ( $objects as $object ) { 01059 if ( substr( $object, -1 ) !== '/' ) { 01060 $files[] = $object; // directories end in '/' 01061 } 01062 } 01063 // Recursive: list all files under $dir and its subdirs 01064 } else { // files 01065 $objects = $container->list_objects( $limit, $after, $prefix ); 01066 $files = $objects; 01067 } 01068 if ( count( $objects ) < $limit ) { 01069 $after = INF; // avoid a second RTT 01070 } else { 01071 $after = end( $objects ); // update last item 01072 } 01073 } catch ( NoSuchContainerException $e ) { 01074 } catch ( CloudFilesException $e ) { // some other exception? 01075 $this->handleException( $e, null, __METHOD__, 01076 array( 'cont' => $fullCont, 'dir' => $dir ) ); 01077 } 01078 01079 wfProfileOut( __METHOD__ . '-' . $this->name ); 01080 return $files; 01081 } 01082 01087 protected function doGetFileSha1base36( array $params ) { 01088 $stat = $this->getFileStat( $params ); 01089 if ( $stat ) { 01090 return $stat['sha1']; 01091 } else { 01092 return false; 01093 } 01094 } 01095 01100 protected function doStreamFile( array $params ) { 01101 $status = Status::newGood(); 01102 01103 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 01104 if ( $srcRel === null ) { 01105 $status->fatal( 'backend-fail-invalidpath', $params['src'] ); 01106 } 01107 01108 try { 01109 $cont = $this->getContainer( $srcCont ); 01110 } catch ( NoSuchContainerException $e ) { 01111 $status->fatal( 'backend-fail-stream', $params['src'] ); 01112 return $status; 01113 } catch ( CloudFilesException $e ) { // some other exception? 01114 $this->handleException( $e, $status, __METHOD__, $params ); 01115 return $status; 01116 } 01117 01118 try { 01119 $output = fopen( 'php://output', 'wb' ); 01120 $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD 01121 $obj->stream( $output, $this->headersFromParams( $params ) ); 01122 } catch ( NoSuchObjectException $e ) { 01123 $status->fatal( 'backend-fail-stream', $params['src'] ); 01124 } catch ( CloudFilesException $e ) { // some other exception? 01125 $this->handleException( $e, $status, __METHOD__, $params ); 01126 } 01127 01128 return $status; 01129 } 01130 01135 protected function doGetLocalCopyMulti( array $params ) { 01136 $tmpFiles = array(); 01137 01138 $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging 01139 // Blindly create tmp files and stream to them, catching any exception if the file does 01140 // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata(). 01141 foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) { 01142 $cfOps = array(); // (path => CF_Async_Op) 01143 01144 foreach ( $pathBatch as $path ) { // each path in this concurrent batch 01145 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path ); 01146 if ( $srcRel === null ) { 01147 $tmpFiles[$path] = null; 01148 continue; 01149 } 01150 $tmpFile = null; 01151 try { 01152 $sContObj = $this->getContainer( $srcCont ); 01153 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD 01154 // Get source file extension 01155 $ext = FileBackend::extensionFromPath( $path ); 01156 // Create a new temporary file... 01157 $tmpFile = TempFSFile::factory( 'localcopy_', $ext ); 01158 if ( $tmpFile ) { 01159 $handle = fopen( $tmpFile->getPath(), 'wb' ); 01160 if ( $handle ) { 01161 $headers = $this->headersFromParams( $params ); 01162 if ( count( $pathBatch ) > 1 ) { 01163 $cfOps[$path] = $obj->stream_async( $handle, $headers ); 01164 $cfOps[$path]->_file_handle = $handle; // close this later 01165 } else { 01166 $obj->stream( $handle, $headers ); 01167 fclose( $handle ); 01168 } 01169 } else { 01170 $tmpFile = null; 01171 } 01172 } 01173 } catch ( NoSuchContainerException $e ) { 01174 $tmpFile = null; 01175 } catch ( NoSuchObjectException $e ) { 01176 $tmpFile = null; 01177 } catch ( CloudFilesException $e ) { // some other exception? 01178 $tmpFile = null; 01179 $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep ); 01180 } 01181 $tmpFiles[$path] = $tmpFile; 01182 } 01183 01184 $batch = new CF_Async_Op_Batch( $cfOps ); 01185 $cfOps = $batch->execute(); 01186 foreach ( $cfOps as $path => $cfOp ) { 01187 try { 01188 $cfOp->getLastResponse(); 01189 } catch ( NoSuchContainerException $e ) { 01190 $tmpFiles[$path] = null; 01191 } catch ( NoSuchObjectException $e ) { 01192 $tmpFiles[$path] = null; 01193 } catch ( CloudFilesException $e ) { // some other exception? 01194 $tmpFiles[$path] = null; 01195 $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep ); 01196 } 01197 fclose( $cfOp->_file_handle ); // close open handle 01198 } 01199 } 01200 01201 return $tmpFiles; 01202 } 01203 01208 public function getFileHttpUrl( array $params ) { 01209 if ( $this->swiftTempUrlKey != '' || 01210 ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' ) ) 01211 { 01212 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] ); 01213 if ( $srcRel === null ) { 01214 return null; // invalid path 01215 } 01216 try { 01217 $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400; 01218 $sContObj = $this->getContainer( $srcCont ); 01219 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD 01220 if ( $this->swiftTempUrlKey != '' ) { 01221 return $obj->get_temp_url( $this->swiftTempUrlKey, $ttl, "GET" ); 01222 } else { // give S3 API URL for rgw 01223 $expires = time() + $ttl; 01224 // Path for signature starts with the bucket 01225 $spath = '/' . rawurlencode( $srcCont ) . '/' . 01226 str_replace( '%2F', '/', rawurlencode( $srcRel ) ); 01227 // Calculate the hash 01228 $signature = base64_encode( hash_hmac( 01229 'sha1', 01230 "GET\n\n\n{$expires}\n{$spath}", 01231 $this->rgwS3SecretKey, 01232 true // raw 01233 ) ); 01234 // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html. 01235 // Note: adding a newline for empty CanonicalizedAmzHeaders does not work. 01236 return wfAppendQuery( 01237 str_replace( '/swift/v1', '', // S3 API is the rgw default 01238 $sContObj->cfs_http->getStorageUrl() . $spath ), 01239 array( 01240 'Signature' => $signature, 01241 'Expires' => $expires, 01242 'AWSAccessKeyId' => $this->rgwS3AccessKey ) 01243 ); 01244 } 01245 } catch ( NoSuchContainerException $e ) { 01246 } catch ( CloudFilesException $e ) { // some other exception? 01247 $this->handleException( $e, null, __METHOD__, $params ); 01248 } 01249 } 01250 return null; 01251 } 01252 01257 protected function directoriesAreVirtual() { 01258 return true; 01259 } 01260 01269 protected function headersFromParams( array $params ) { 01270 $hdrs = array(); 01271 if ( !empty( $params['latest'] ) ) { 01272 $hdrs[] = 'X-Newest: true'; 01273 } 01274 return $hdrs; 01275 } 01276 01281 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) { 01282 $statuses = array(); 01283 01284 $cfOps = array(); // list of CF_Async_Op objects 01285 foreach ( $fileOpHandles as $index => $fileOpHandle ) { 01286 $cfOps[$index] = $fileOpHandle->cfOp; 01287 } 01288 $batch = new CF_Async_Op_Batch( $cfOps ); 01289 01290 $cfOps = $batch->execute(); 01291 foreach ( $cfOps as $index => $cfOp ) { 01292 $status = Status::newGood(); 01293 $function = '_getResponse' . $fileOpHandles[$index]->call; 01294 try { // catch exceptions; update status 01295 $this->$function( $cfOp, $status, $fileOpHandles[$index]->params ); 01296 $this->purgeCDNCache( $fileOpHandles[$index]->affectedObjects ); 01297 } catch ( CloudFilesException $e ) { // some other exception? 01298 $this->handleException( $e, $status, 01299 __CLASS__ . ":$function", $fileOpHandles[$index]->params ); 01300 } 01301 $statuses[$index] = $status; 01302 } 01303 01304 return $statuses; 01305 } 01306 01333 protected function setContainerAccess( 01334 CF_Container $contObj, array $readGrps, array $writeGrps 01335 ) { 01336 $creds = $contObj->cfs_auth->export_credentials(); 01337 01338 $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name ); 01339 01340 // Note: 10 second timeout consistent with php-cloudfiles 01341 $req = MWHttpRequest::factory( $url, array( 'method' => 'POST', 'timeout' => 10 ) ); 01342 $req->setHeader( 'X-Auth-Token', $creds['auth_token'] ); 01343 $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) ); 01344 $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) ); 01345 01346 return $req->execute(); // should return 204 01347 } 01348 01356 public function purgeCDNCache( array $objects ) { 01357 if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) { 01358 foreach ( $objects as $object ) { 01359 try { 01360 $object->purge_from_cdn(); 01361 } catch ( CDNNotEnabledException $e ) { 01362 // CDN not enabled; nothing to see here 01363 } catch ( CloudFilesException $e ) { 01364 $this->handleException( $e, null, __METHOD__, 01365 array( 'cont' => $object->container->name, 'obj' => $object->name ) ); 01366 } 01367 } 01368 } 01369 } 01370 01378 protected function getConnection() { 01379 if ( $this->connException instanceof CloudFilesException ) { 01380 if ( ( time() - $this->connErrorTime ) < 60 ) { 01381 throw $this->connException; // failed last attempt; don't bother 01382 } else { // actually retry this time 01383 $this->connException = null; 01384 $this->connErrorTime = 0; 01385 } 01386 } 01387 // Session keys expire after a while, so we renew them periodically 01388 $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL ); 01389 // Authenticate with proxy and get a session key... 01390 if ( !$this->conn || $reAuth ) { 01391 $this->sessionStarted = 0; 01392 $this->connContainerCache->clear(); 01393 $cacheKey = $this->getCredsCacheKey( $this->auth->username ); 01394 $creds = $this->srvCache->get( $cacheKey ); // credentials 01395 if ( is_array( $creds ) ) { // cache hit 01396 $this->auth->load_cached_credentials( 01397 $creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] ); 01398 $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case 01399 } else { // cache miss 01400 try { 01401 $this->auth->authenticate(); 01402 $creds = $this->auth->export_credentials(); 01403 $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache 01404 $this->sessionStarted = time(); 01405 } catch ( CloudFilesException $e ) { 01406 $this->connException = $e; // don't keep re-trying 01407 $this->connErrorTime = time(); 01408 throw $e; // throw it back 01409 } 01410 } 01411 if ( $this->conn ) { // re-authorizing? 01412 $this->conn->close(); // close active cURL handles in CF_Http object 01413 } 01414 $this->conn = new CF_Connection( $this->auth ); 01415 } 01416 return $this->conn; 01417 } 01418 01424 protected function closeConnection() { 01425 if ( $this->conn ) { 01426 $this->conn->close(); // close active cURL handles in CF_Http object 01427 $this->conn = null; 01428 $this->sessionStarted = 0; 01429 $this->connContainerCache->clear(); 01430 } 01431 } 01432 01439 private function getCredsCacheKey( $username ) { 01440 return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username ); 01441 } 01442 01452 protected function getContainer( $container, $bypassCache = false ) { 01453 $conn = $this->getConnection(); // Swift proxy connection 01454 if ( $bypassCache ) { // purge cache 01455 $this->connContainerCache->clear( $container ); 01456 } elseif ( !$this->connContainerCache->has( $container, 'obj' ) ) { 01457 $this->primeContainerCache( array( $container ) ); // check persistent cache 01458 } 01459 if ( !$this->connContainerCache->has( $container, 'obj' ) ) { 01460 $contObj = $conn->get_container( $container ); 01461 // NoSuchContainerException not thrown: container must exist 01462 $this->connContainerCache->set( $container, 'obj', $contObj ); // cache it 01463 if ( !$bypassCache ) { 01464 $this->setContainerCache( $container, // update persistent cache 01465 array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count ) 01466 ); 01467 } 01468 } 01469 return $this->connContainerCache->get( $container, 'obj' ); 01470 } 01471 01479 protected function createContainer( $container ) { 01480 $conn = $this->getConnection(); // Swift proxy connection 01481 $contObj = $conn->create_container( $container ); 01482 $this->connContainerCache->set( $container, 'obj', $contObj ); // cache 01483 return $contObj; 01484 } 01485 01493 protected function deleteContainer( $container ) { 01494 $conn = $this->getConnection(); // Swift proxy connection 01495 $this->connContainerCache->clear( $container ); // purge 01496 $conn->delete_container( $container ); 01497 } 01498 01503 protected function doPrimeContainerCache( array $containerInfo ) { 01504 try { 01505 $conn = $this->getConnection(); // Swift proxy connection 01506 foreach ( $containerInfo as $container => $info ) { 01507 $contObj = new CF_Container( $conn->cfs_auth, $conn->cfs_http, 01508 $container, $info['count'], $info['bytes'] ); 01509 $this->connContainerCache->set( $container, 'obj', $contObj ); 01510 } 01511 } catch ( CloudFilesException $e ) { // some other exception? 01512 $this->handleException( $e, null, __METHOD__, array() ); 01513 } 01514 } 01515 01526 protected function handleException( Exception $e, $status, $func, array $params ) { 01527 if ( $status instanceof Status ) { 01528 if ( $e instanceof AuthenticationException ) { 01529 $status->fatal( 'backend-fail-connect', $this->name ); 01530 } else { 01531 $status->fatal( 'backend-fail-internal', $this->name ); 01532 } 01533 } 01534 if ( $e->getMessage() ) { 01535 trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING ); 01536 } 01537 if ( $e instanceof InvalidResponseException ) { // possibly a stale token 01538 $this->srvCache->delete( $this->getCredsCacheKey( $this->auth->username ) ); 01539 $this->closeConnection(); // force a re-connect and re-auth next time 01540 } 01541 wfDebugLog( 'SwiftBackend', 01542 get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" . 01543 ( $e->getMessage() ? ": {$e->getMessage()}" : "" ) 01544 ); 01545 } 01546 } 01547 01551 class SwiftFileOpHandle extends FileBackendStoreOpHandle { 01553 public $cfOp; 01555 public $affectedObjects = array(); 01556 01557 public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) { 01558 $this->backend = $backend; 01559 $this->params = $params; 01560 $this->call = $call; 01561 $this->cfOp = $cfOp; 01562 } 01563 } 01564 01572 abstract class SwiftFileBackendList implements Iterator { 01574 protected $bufferIter = array(); 01575 protected $bufferAfter = null; // string; list items *after* this path 01576 protected $pos = 0; // integer 01578 protected $params = array(); 01579 01581 protected $backend; 01582 protected $container; // string; container name 01583 protected $dir; // string; storage directory 01584 protected $suffixStart; // integer 01585 01586 const PAGE_SIZE = 9000; // file listing buffer size 01587 01594 public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) { 01595 $this->backend = $backend; 01596 $this->container = $fullCont; 01597 $this->dir = $dir; 01598 if ( substr( $this->dir, -1 ) === '/' ) { 01599 $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash 01600 } 01601 if ( $this->dir == '' ) { // whole container 01602 $this->suffixStart = 0; 01603 } else { // dir within container 01604 $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/" 01605 } 01606 $this->params = $params; 01607 } 01608 01613 public function key() { 01614 return $this->pos; 01615 } 01616 01621 public function next() { 01622 // Advance to the next file in the page 01623 next( $this->bufferIter ); 01624 ++$this->pos; 01625 // Check if there are no files left in this page and 01626 // advance to the next page if this page was not empty. 01627 if ( !$this->valid() && count( $this->bufferIter ) ) { 01628 $this->bufferIter = $this->pageFromList( 01629 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params 01630 ); // updates $this->bufferAfter 01631 } 01632 } 01633 01638 public function rewind() { 01639 $this->pos = 0; 01640 $this->bufferAfter = null; 01641 $this->bufferIter = $this->pageFromList( 01642 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params 01643 ); // updates $this->bufferAfter 01644 } 01645 01650 public function valid() { 01651 if ( $this->bufferIter === null ) { 01652 return false; // some failure? 01653 } else { 01654 return ( current( $this->bufferIter ) !== false ); // no paths can have this value 01655 } 01656 } 01657 01668 abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params ); 01669 } 01670 01674 class SwiftFileBackendDirList extends SwiftFileBackendList { 01679 public function current() { 01680 return substr( current( $this->bufferIter ), $this->suffixStart, -1 ); 01681 } 01682 01687 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) { 01688 return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params ); 01689 } 01690 } 01691 01695 class SwiftFileBackendFileList extends SwiftFileBackendList { 01700 public function current() { 01701 return substr( current( $this->bufferIter ), $this->suffixStart ); 01702 } 01703 01708 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) { 01709 return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params ); 01710 } 01711 }