MediaWiki  REL1_23
SwiftFileBackend.php
Go to the documentation of this file.
00001 <?php
00035 class SwiftFileBackend extends FileBackendStore {
00037     protected $http;
00038 
00040     protected $authTTL;
00041 
00043     protected $swiftAuthUrl;
00044 
00046     protected $swiftUser;
00047 
00049     protected $swiftKey;
00050 
00052     protected $swiftTempUrlKey;
00053 
00055     protected $rgwS3AccessKey;
00056 
00058     protected $rgwS3SecretKey;
00059 
00061     protected $srvCache;
00062 
00064     protected $containerStatCache;
00065 
00067     protected $authCreds;
00068 
00070     protected $authSessionTimestamp = 0;
00071 
00073     protected $authErrorTimestamp = null;
00074 
00076     protected $isRGW = false;
00077 
00106     public function __construct( array $config ) {
00107         parent::__construct( $config );
00108         // Required settings
00109         $this->swiftAuthUrl = $config['swiftAuthUrl'];
00110         $this->swiftUser = $config['swiftUser'];
00111         $this->swiftKey = $config['swiftKey'];
00112         // Optional settings
00113         $this->authTTL = isset( $config['swiftAuthTTL'] )
00114             ? $config['swiftAuthTTL']
00115             : 5 * 60; // some sane number
00116         $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
00117             ? $config['swiftTempUrlKey']
00118             : '';
00119         $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
00120             ? $config['shardViaHashLevels']
00121             : '';
00122         $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
00123             ? $config['rgwS3AccessKey']
00124             : '';
00125         $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
00126             ? $config['rgwS3SecretKey']
00127             : '';
00128         // HTTP helper client
00129         $this->http = new MultiHttpClient( array() );
00130         // Cache container information to mask latency
00131         $this->memCache = wfGetMainCache();
00132         // Process cache for container info
00133         $this->containerStatCache = new ProcessCacheLRU( 300 );
00134         // Cache auth token information to avoid RTTs
00135         if ( !empty( $config['cacheAuthInfo'] ) ) {
00136             if ( PHP_SAPI === 'cli' ) {
00137                 $this->srvCache = wfGetMainCache(); // preferrably memcached
00138             } else {
00139                 try { // look for APC, XCache, WinCache, ect...
00140                     $this->srvCache = ObjectCache::newAccelerator( array() );
00141                 } catch ( Exception $e ) {
00142                 }
00143             }
00144         }
00145         $this->srvCache = $this->srvCache ?: new EmptyBagOStuff();
00146     }
00147 
00148     public function getFeatures() {
00149         return ( FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
00150     }
00151 
00152     protected function resolveContainerPath( $container, $relStoragePath ) {
00153         if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF
00154             return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
00155         } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
00156             return null; // too long for Swift
00157         }
00158 
00159         return $relStoragePath;
00160     }
00161 
00162     public function isPathUsableInternal( $storagePath ) {
00163         list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
00164         if ( $rel === null ) {
00165             return false; // invalid
00166         }
00167 
00168         return is_array( $this->getContainerStat( $container ) );
00169     }
00170 
00178     protected function sanitizeHdrs( array $params ) {
00179         $headers = array();
00180 
00181         // Normalize casing, and strip out illegal headers
00182         if ( isset( $params['headers'] ) ) {
00183             foreach ( $params['headers'] as $name => $value ) {
00184                 $name = strtolower( $name );
00185                 if ( preg_match( '/^content-(type|length)$/', $name ) ) {
00186                     continue; // blacklisted
00187                 } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
00188                     $headers[$name] = $value; // allowed
00189                 } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
00190                     $headers[$name] = $value; // allowed
00191                 }
00192             }
00193         }
00194         // By default, Swift has annoyingly low maximum header value limits
00195         if ( isset( $headers['content-disposition'] ) ) {
00196             $disposition = '';
00197             foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
00198                 $part = trim( $part );
00199                 $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
00200                 if ( strlen( $new ) <= 255 ) {
00201                     $disposition = $new;
00202                 } else {
00203                     break; // too long; sigh
00204                 }
00205             }
00206             $headers['content-disposition'] = $disposition;
00207         }
00208 
00209         return $headers;
00210     }
00211 
00212     protected function doCreateInternal( array $params ) {
00213         $status = Status::newGood();
00214 
00215         list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00216         if ( $dstRel === null ) {
00217             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00218 
00219             return $status;
00220         }
00221 
00222         $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
00223         $contentType = $this->getContentType( $params['dst'], $params['content'], null );
00224 
00225         $reqs = array( array(
00226             'method' => 'PUT',
00227             'url' => array( $dstCont, $dstRel ),
00228             'headers' => array(
00229                 'content-length' => strlen( $params['content'] ),
00230                 'etag' => md5( $params['content'] ),
00231                 'content-type' => $contentType,
00232                 'x-object-meta-sha1base36' => $sha1Hash
00233             ) + $this->sanitizeHdrs( $params ),
00234             'body' => $params['content']
00235         ) );
00236 
00237         $be = $this;
00238         $method = __METHOD__;
00239         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00240             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00241             if ( $rcode === 201 ) {
00242                 // good
00243             } elseif ( $rcode === 412 ) {
00244                 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00245             } else {
00246                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00247             }
00248         };
00249 
00250         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00251         if ( !empty( $params['async'] ) ) { // deferred
00252             $status->value = $opHandle;
00253         } else { // actually write the object in Swift
00254             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00255         }
00256 
00257         return $status;
00258     }
00259 
00260     protected function doStoreInternal( array $params ) {
00261         $status = Status::newGood();
00262 
00263         list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00264         if ( $dstRel === null ) {
00265             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00266 
00267             return $status;
00268         }
00269 
00270         wfSuppressWarnings();
00271         $sha1Hash = sha1_file( $params['src'] );
00272         wfRestoreWarnings();
00273         if ( $sha1Hash === false ) { // source doesn't exist?
00274             $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00275 
00276             return $status;
00277         }
00278         $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
00279         $contentType = $this->getContentType( $params['dst'], null, $params['src'] );
00280 
00281         $handle = fopen( $params['src'], 'rb' );
00282         if ( $handle === false ) { // source doesn't exist?
00283             $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
00284 
00285             return $status;
00286         }
00287 
00288         $reqs = array( array(
00289             'method' => 'PUT',
00290             'url' => array( $dstCont, $dstRel ),
00291             'headers' => array(
00292                 'content-length' => filesize( $params['src'] ),
00293                 'etag' => md5_file( $params['src'] ),
00294                 'content-type' => $contentType,
00295                 'x-object-meta-sha1base36' => $sha1Hash
00296             ) + $this->sanitizeHdrs( $params ),
00297             'body' => $handle // resource
00298         ) );
00299 
00300         $be = $this;
00301         $method = __METHOD__;
00302         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00303             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00304             if ( $rcode === 201 ) {
00305                 // good
00306             } elseif ( $rcode === 412 ) {
00307                 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00308             } else {
00309                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00310             }
00311         };
00312 
00313         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00314         if ( !empty( $params['async'] ) ) { // deferred
00315             $status->value = $opHandle;
00316         } else { // actually write the object in Swift
00317             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00318         }
00319 
00320         return $status;
00321     }
00322 
00323     protected function doCopyInternal( array $params ) {
00324         $status = Status::newGood();
00325 
00326         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00327         if ( $srcRel === null ) {
00328             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00329 
00330             return $status;
00331         }
00332 
00333         list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00334         if ( $dstRel === null ) {
00335             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00336 
00337             return $status;
00338         }
00339 
00340         $reqs = array( array(
00341             'method' => 'PUT',
00342             'url' => array( $dstCont, $dstRel ),
00343             'headers' => array(
00344                 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
00345                     '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
00346             ) + $this->sanitizeHdrs( $params ), // extra headers merged into object
00347         ) );
00348 
00349         $be = $this;
00350         $method = __METHOD__;
00351         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00352             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00353             if ( $rcode === 201 ) {
00354                 // good
00355             } elseif ( $rcode === 404 ) {
00356                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00357             } else {
00358                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00359             }
00360         };
00361 
00362         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00363         if ( !empty( $params['async'] ) ) { // deferred
00364             $status->value = $opHandle;
00365         } else { // actually write the object in Swift
00366             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00367         }
00368 
00369         return $status;
00370     }
00371 
00372     protected function doMoveInternal( array $params ) {
00373         $status = Status::newGood();
00374 
00375         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00376         if ( $srcRel === null ) {
00377             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00378 
00379             return $status;
00380         }
00381 
00382         list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00383         if ( $dstRel === null ) {
00384             $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00385 
00386             return $status;
00387         }
00388 
00389         $reqs = array(
00390             array(
00391                 'method' => 'PUT',
00392                 'url' => array( $dstCont, $dstRel ),
00393                 'headers' => array(
00394                     'x-copy-from' => '/' . rawurlencode( $srcCont ) .
00395                         '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
00396                 ) + $this->sanitizeHdrs( $params ) // extra headers merged into object
00397             )
00398         );
00399         if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
00400             $reqs[] = array(
00401                 'method' => 'DELETE',
00402                 'url' => array( $srcCont, $srcRel ),
00403                 'headers' => array()
00404             );
00405         }
00406 
00407         $be = $this;
00408         $method = __METHOD__;
00409         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00410             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00411             if ( $request['method'] === 'PUT' && $rcode === 201 ) {
00412                 // good
00413             } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
00414                 // good
00415             } elseif ( $rcode === 404 ) {
00416                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00417             } else {
00418                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00419             }
00420         };
00421 
00422         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00423         if ( !empty( $params['async'] ) ) { // deferred
00424             $status->value = $opHandle;
00425         } else { // actually move the object in Swift
00426             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00427         }
00428 
00429         return $status;
00430     }
00431 
00432     protected function doDeleteInternal( array $params ) {
00433         $status = Status::newGood();
00434 
00435         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00436         if ( $srcRel === null ) {
00437             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00438 
00439             return $status;
00440         }
00441 
00442         $reqs = array( array(
00443             'method' => 'DELETE',
00444             'url' => array( $srcCont, $srcRel ),
00445             'headers' => array()
00446         ) );
00447 
00448         $be = $this;
00449         $method = __METHOD__;
00450         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00451             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00452             if ( $rcode === 204 ) {
00453                 // good
00454             } elseif ( $rcode === 404 ) {
00455                 if ( empty( $params['ignoreMissingSource'] ) ) {
00456                     $status->fatal( 'backend-fail-delete', $params['src'] );
00457                 }
00458             } else {
00459                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00460             }
00461         };
00462 
00463         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00464         if ( !empty( $params['async'] ) ) { // deferred
00465             $status->value = $opHandle;
00466         } else { // actually delete the object in Swift
00467             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00468         }
00469 
00470         return $status;
00471     }
00472 
00473     protected function doDescribeInternal( array $params ) {
00474         $status = Status::newGood();
00475 
00476         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00477         if ( $srcRel === null ) {
00478             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00479 
00480             return $status;
00481         }
00482 
00483         // Fetch the old object headers/metadata...this should be in stat cache by now
00484         $stat = $this->getFileStat( array( 'src' => $params['src'], 'latest' => 1 ) );
00485         if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
00486             $stat = $this->doGetFileStat( array( 'src' => $params['src'], 'latest' => 1 ) );
00487         }
00488         if ( !$stat ) {
00489             $status->fatal( 'backend-fail-describe', $params['src'] );
00490 
00491             return $status;
00492         }
00493 
00494         // POST clears prior headers, so we need to merge the changes in to the old ones
00495         $metaHdrs = array();
00496         foreach ( $stat['xattr']['metadata'] as $name => $value ) {
00497             $metaHdrs["x-object-meta-$name"] = $value;
00498         }
00499         $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
00500 
00501         $reqs = array( array(
00502             'method' => 'POST',
00503             'url' => array( $srcCont, $srcRel ),
00504             'headers' => $metaHdrs + $customHdrs
00505         ) );
00506 
00507         $be = $this;
00508         $method = __METHOD__;
00509         $handler = function ( array $request, Status $status ) use ( $be, $method, $params ) {
00510             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
00511             if ( $rcode === 202 ) {
00512                 // good
00513             } elseif ( $rcode === 404 ) {
00514                 $status->fatal( 'backend-fail-describe', $params['src'] );
00515             } else {
00516                 $be->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
00517             }
00518         };
00519 
00520         $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
00521         if ( !empty( $params['async'] ) ) { // deferred
00522             $status->value = $opHandle;
00523         } else { // actually change the object in Swift
00524             $status->merge( current( $this->doExecuteOpHandlesInternal( array( $opHandle ) ) ) );
00525         }
00526 
00527         return $status;
00528     }
00529 
00530     protected function doPrepareInternal( $fullCont, $dir, array $params ) {
00531         $status = Status::newGood();
00532 
00533         // (a) Check if container already exists
00534         $stat = $this->getContainerStat( $fullCont );
00535         if ( is_array( $stat ) ) {
00536             return $status; // already there
00537         } elseif ( $stat === null ) {
00538             $status->fatal( 'backend-fail-internal', $this->name );
00539 
00540             return $status;
00541         }
00542 
00543         // (b) Create container as needed with proper ACLs
00544         if ( $stat === false ) {
00545             $params['op'] = 'prepare';
00546             $status->merge( $this->createContainer( $fullCont, $params ) );
00547         }
00548 
00549         return $status;
00550     }
00551 
00552     protected function doSecureInternal( $fullCont, $dir, array $params ) {
00553         $status = Status::newGood();
00554         if ( empty( $params['noAccess'] ) ) {
00555             return $status; // nothing to do
00556         }
00557 
00558         $stat = $this->getContainerStat( $fullCont );
00559         if ( is_array( $stat ) ) {
00560             // Make container private to end-users...
00561             $status->merge( $this->setContainerAccess(
00562                 $fullCont,
00563                 array( $this->swiftUser ), // read
00564                 array( $this->swiftUser ) // write
00565             ) );
00566         } elseif ( $stat === false ) {
00567             $status->fatal( 'backend-fail-usable', $params['dir'] );
00568         } else {
00569             $status->fatal( 'backend-fail-internal', $this->name );
00570         }
00571 
00572         return $status;
00573     }
00574 
00575     protected function doPublishInternal( $fullCont, $dir, array $params ) {
00576         $status = Status::newGood();
00577 
00578         $stat = $this->getContainerStat( $fullCont );
00579         if ( is_array( $stat ) ) {
00580             // Make container public to end-users...
00581             $status->merge( $this->setContainerAccess(
00582                 $fullCont,
00583                 array( $this->swiftUser, '.r:*' ), // read
00584                 array( $this->swiftUser ) // write
00585             ) );
00586         } elseif ( $stat === false ) {
00587             $status->fatal( 'backend-fail-usable', $params['dir'] );
00588         } else {
00589             $status->fatal( 'backend-fail-internal', $this->name );
00590         }
00591 
00592         return $status;
00593     }
00594 
00595     protected function doCleanInternal( $fullCont, $dir, array $params ) {
00596         $status = Status::newGood();
00597 
00598         // Only containers themselves can be removed, all else is virtual
00599         if ( $dir != '' ) {
00600             return $status; // nothing to do
00601         }
00602 
00603         // (a) Check the container
00604         $stat = $this->getContainerStat( $fullCont, true );
00605         if ( $stat === false ) {
00606             return $status; // ok, nothing to do
00607         } elseif ( !is_array( $stat ) ) {
00608             $status->fatal( 'backend-fail-internal', $this->name );
00609 
00610             return $status;
00611         }
00612 
00613         // (b) Delete the container if empty
00614         if ( $stat['count'] == 0 ) {
00615             $params['op'] = 'clean';
00616             $status->merge( $this->deleteContainer( $fullCont, $params ) );
00617         }
00618 
00619         return $status;
00620     }
00621 
00622     protected function doGetFileStat( array $params ) {
00623         $params = array( 'srcs' => array( $params['src'] ), 'concurrency' => 1 ) + $params;
00624         unset( $params['src'] );
00625         $stats = $this->doGetFileStatMulti( $params );
00626 
00627         return reset( $stats );
00628     }
00629 
00640     protected function convertSwiftDate( $ts, $format = TS_MW ) {
00641         try {
00642             $timestamp = new MWTimestamp( $ts );
00643 
00644             return $timestamp->getTimestamp( $format );
00645         } catch ( MWException $e ) {
00646             throw new FileBackendError( $e->getMessage() );
00647         }
00648     }
00649 
00657     protected function addMissingMetadata( array $objHdrs, $path ) {
00658         if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
00659             return $objHdrs; // nothing to do
00660         }
00661 
00662         $section = new ProfileSection( __METHOD__ . '-' . $this->name );
00663         trigger_error( "$path was not stored with SHA-1 metadata.", E_USER_WARNING );
00664 
00665         $auth = $this->getAuthentication();
00666         if ( !$auth ) {
00667             $objHdrs['x-object-meta-sha1base36'] = false;
00668 
00669             return $objHdrs; // failed
00670         }
00671 
00672         $status = Status::newGood();
00673         $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
00674         if ( $status->isOK() ) {
00675             $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
00676             if ( $tmpFile ) {
00677                 $hash = $tmpFile->getSha1Base36();
00678                 if ( $hash !== false ) {
00679                     $objHdrs['x-object-meta-sha1base36'] = $hash;
00680                     list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
00681                     list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
00682                         'method' => 'POST',
00683                         'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
00684                         'headers' => $this->authTokenHeaders( $auth ) + $objHdrs
00685                     ) );
00686                     if ( $rcode >= 200 && $rcode <= 299 ) {
00687                         return $objHdrs; // success
00688                     }
00689                 }
00690             }
00691         }
00692         trigger_error( "Unable to set SHA-1 metadata for $path", E_USER_WARNING );
00693         $objHdrs['x-object-meta-sha1base36'] = false;
00694 
00695         return $objHdrs; // failed
00696     }
00697 
00698     protected function doGetFileContentsMulti( array $params ) {
00699         $contents = array();
00700 
00701         $auth = $this->getAuthentication();
00702 
00703         $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
00704         // Blindly create tmp files and stream to them, catching any exception if the file does
00705         // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
00706         $reqs = array(); // (path => op)
00707 
00708         foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
00709             list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
00710             if ( $srcRel === null || !$auth ) {
00711                 $contents[$path] = false;
00712                 continue;
00713             }
00714             // Create a new temporary memory file...
00715             $handle = fopen( 'php://temp', 'wb' );
00716             if ( $handle ) {
00717                 $reqs[$path] = array(
00718                     'method'  => 'GET',
00719                     'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
00720                     'headers' => $this->authTokenHeaders( $auth )
00721                         + $this->headersFromParams( $params ),
00722                     'stream'  => $handle,
00723                 );
00724             }
00725             $contents[$path] = false;
00726         }
00727 
00728         $opts = array( 'maxConnsPerHost' => $params['concurrency'] );
00729         $reqs = $this->http->runMulti( $reqs, $opts );
00730         foreach ( $reqs as $path => $op ) {
00731             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
00732             if ( $rcode >= 200 && $rcode <= 299 ) {
00733                 rewind( $op['stream'] ); // start from the beginning
00734                 $contents[$path] = stream_get_contents( $op['stream'] );
00735             } elseif ( $rcode === 404 ) {
00736                 $contents[$path] = false;
00737             } else {
00738                 $this->onError( null, __METHOD__,
00739                     array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
00740             }
00741             fclose( $op['stream'] ); // close open handle
00742         }
00743 
00744         return $contents;
00745     }
00746 
00747     protected function doDirectoryExists( $fullCont, $dir, array $params ) {
00748         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00749         $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
00750         if ( $status->isOk() ) {
00751             return ( count( $status->value ) ) > 0;
00752         }
00753 
00754         return null; // error
00755     }
00756 
00764     public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
00765         return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
00766     }
00767 
00775     public function getFileListInternal( $fullCont, $dir, array $params ) {
00776         return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
00777     }
00778 
00790     public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00791         $dirs = array();
00792         if ( $after === INF ) {
00793             return $dirs; // nothing more
00794         }
00795 
00796         $section = new ProfileSection( __METHOD__ . '-' . $this->name );
00797 
00798         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00799         // Non-recursive: only list dirs right under $dir
00800         if ( !empty( $params['topOnly'] ) ) {
00801             $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
00802             if ( !$status->isOk() ) {
00803                 return $dirs; // error
00804             }
00805             $objects = $status->value;
00806             foreach ( $objects as $object ) { // files and directories
00807                 if ( substr( $object, -1 ) === '/' ) {
00808                     $dirs[] = $object; // directories end in '/'
00809                 }
00810             }
00811         } else {
00812             // Recursive: list all dirs under $dir and its subdirs
00813             $getParentDir = function ( $path ) {
00814                 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
00815             };
00816 
00817             // Get directory from last item of prior page
00818             $lastDir = $getParentDir( $after ); // must be first page
00819             $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
00820 
00821             if ( !$status->isOk() ) {
00822                 return $dirs; // error
00823             }
00824 
00825             $objects = $status->value;
00826 
00827             foreach ( $objects as $object ) { // files
00828                 $objectDir = $getParentDir( $object ); // directory of object
00829 
00830                 if ( $objectDir !== false && $objectDir !== $dir ) {
00831                     // Swift stores paths in UTF-8, using binary sorting.
00832                     // See function "create_container_table" in common/db.py.
00833                     // If a directory is not "greater" than the last one,
00834                     // then it was already listed by the calling iterator.
00835                     if ( strcmp( $objectDir, $lastDir ) > 0 ) {
00836                         $pDir = $objectDir;
00837                         do { // add dir and all its parent dirs
00838                             $dirs[] = "{$pDir}/";
00839                             $pDir = $getParentDir( $pDir );
00840                         } while ( $pDir !== false // sanity
00841                             && strcmp( $pDir, $lastDir ) > 0 // not done already
00842                             && strlen( $pDir ) > strlen( $dir ) // within $dir
00843                         );
00844                     }
00845                     $lastDir = $objectDir;
00846                 }
00847             }
00848         }
00849         // Page on the unfiltered directory listing (what is returned may be filtered)
00850         if ( count( $objects ) < $limit ) {
00851             $after = INF; // avoid a second RTT
00852         } else {
00853             $after = end( $objects ); // update last item
00854         }
00855 
00856         return $dirs;
00857     }
00858 
00870     public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00871         $files = array(); // list of (path, stat array or null) entries
00872         if ( $after === INF ) {
00873             return $files; // nothing more
00874         }
00875 
00876         $section = new ProfileSection( __METHOD__ . '-' . $this->name );
00877 
00878         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00879         // $objects will contain a list of unfiltered names or CF_Object items
00880         // Non-recursive: only list files right under $dir
00881         if ( !empty( $params['topOnly'] ) ) {
00882             if ( !empty( $params['adviseStat'] ) ) {
00883                 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
00884             } else {
00885                 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
00886             }
00887         } else {
00888             // Recursive: list all files under $dir and its subdirs
00889             if ( !empty( $params['adviseStat'] ) ) {
00890                 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
00891             } else {
00892                 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
00893             }
00894         }
00895 
00896         // Reformat this list into a list of (name, stat array or null) entries
00897         if ( !$status->isOk() ) {
00898             return $files; // error
00899         }
00900 
00901         $objects = $status->value;
00902         $files = $this->buildFileObjectListing( $params, $dir, $objects );
00903 
00904         // Page on the unfiltered object listing (what is returned may be filtered)
00905         if ( count( $objects ) < $limit ) {
00906             $after = INF; // avoid a second RTT
00907         } else {
00908             $after = end( $objects ); // update last item
00909             $after = is_object( $after ) ? $after->name : $after;
00910         }
00911 
00912         return $files;
00913     }
00914 
00924     private function buildFileObjectListing( array $params, $dir, array $objects ) {
00925         $names = array();
00926         foreach ( $objects as $object ) {
00927             if ( is_object( $object ) ) {
00928                 if ( isset( $object->subdir ) || !isset( $object->name ) ) {
00929                     continue; // virtual directory entry; ignore
00930                 }
00931                 $stat = array(
00932                     // Convert various random Swift dates to TS_MW
00933                     'mtime'  => $this->convertSwiftDate( $object->last_modified, TS_MW ),
00934                     'size'   => (int)$object->bytes,
00935                     // Note: manifiest ETags are not an MD5 of the file
00936                     'md5'    => ctype_xdigit( $object->hash ) ? $object->hash : null,
00937                     'latest' => false // eventually consistent
00938                 );
00939                 $names[] = array( $object->name, $stat );
00940             } elseif ( substr( $object, -1 ) !== '/' ) {
00941                 // Omit directories, which end in '/' in listings
00942                 $names[] = array( $object, null );
00943             }
00944         }
00945 
00946         return $names;
00947     }
00948 
00955     public function loadListingStatInternal( $path, array $val ) {
00956         $this->cheapCache->set( $path, 'stat', $val );
00957     }
00958 
00959     protected function doGetFileXAttributes( array $params ) {
00960         $stat = $this->getFileStat( $params );
00961         if ( $stat ) {
00962             if ( !isset( $stat['xattr'] ) ) {
00963                 // Stat entries filled by file listings don't include metadata/headers
00964                 $this->clearCache( array( $params['src'] ) );
00965                 $stat = $this->getFileStat( $params );
00966             }
00967 
00968             return $stat['xattr'];
00969         } else {
00970             return false;
00971         }
00972     }
00973 
00974     protected function doGetFileSha1base36( array $params ) {
00975         $stat = $this->getFileStat( $params );
00976         if ( $stat ) {
00977             if ( !isset( $stat['sha1'] ) ) {
00978                 // Stat entries filled by file listings don't include SHA1
00979                 $this->clearCache( array( $params['src'] ) );
00980                 $stat = $this->getFileStat( $params );
00981             }
00982 
00983             return $stat['sha1'];
00984         } else {
00985             return false;
00986         }
00987     }
00988 
00989     protected function doStreamFile( array $params ) {
00990         $status = Status::newGood();
00991 
00992         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00993         if ( $srcRel === null ) {
00994             $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00995         }
00996 
00997         $auth = $this->getAuthentication();
00998         if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
00999             $status->fatal( 'backend-fail-stream', $params['src'] );
01000 
01001             return $status;
01002         }
01003 
01004         $handle = fopen( 'php://output', 'wb' );
01005 
01006         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01007             'method' => 'GET',
01008             'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
01009             'headers' => $this->authTokenHeaders( $auth )
01010                 + $this->headersFromParams( $params ),
01011             'stream' => $handle,
01012         ) );
01013 
01014         if ( $rcode >= 200 && $rcode <= 299 ) {
01015             // good
01016         } elseif ( $rcode === 404 ) {
01017             $status->fatal( 'backend-fail-stream', $params['src'] );
01018         } else {
01019             $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
01020         }
01021 
01022         return $status;
01023     }
01024 
01025     protected function doGetLocalCopyMulti( array $params ) {
01026         $tmpFiles = array();
01027 
01028         $auth = $this->getAuthentication();
01029 
01030         $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
01031         // Blindly create tmp files and stream to them, catching any exception if the file does
01032         // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
01033         $reqs = array(); // (path => op)
01034 
01035         foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
01036             list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
01037             if ( $srcRel === null || !$auth ) {
01038                 $tmpFiles[$path] = null;
01039                 continue;
01040             }
01041             // Get source file extension
01042             $ext = FileBackend::extensionFromPath( $path );
01043             // Create a new temporary file...
01044             $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
01045             if ( $tmpFile ) {
01046                 $handle = fopen( $tmpFile->getPath(), 'wb' );
01047                 if ( $handle ) {
01048                     $reqs[$path] = array(
01049                         'method'  => 'GET',
01050                         'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
01051                         'headers' => $this->authTokenHeaders( $auth )
01052                             + $this->headersFromParams( $params ),
01053                         'stream'  => $handle,
01054                     );
01055                 } else {
01056                     $tmpFile = null;
01057                 }
01058             }
01059             $tmpFiles[$path] = $tmpFile;
01060         }
01061 
01062         $opts = array( 'maxConnsPerHost' => $params['concurrency'] );
01063         $reqs = $this->http->runMulti( $reqs, $opts );
01064         foreach ( $reqs as $path => $op ) {
01065             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
01066             fclose( $op['stream'] ); // close open handle
01067             if ( $rcode >= 200 && $rcode <= 299
01068                 // double check that the disk is not full/broken
01069                 && $tmpFiles[$path]->getSize() == $rhdrs['content-length']
01070             ) {
01071                 // good
01072             } elseif ( $rcode === 404 ) {
01073                 $tmpFiles[$path] = false;
01074             } else {
01075                 $tmpFiles[$path] = null;
01076                 $this->onError( null, __METHOD__,
01077                     array( 'src' => $path ) + $ep, $rerr, $rcode, $rdesc );
01078             }
01079         }
01080 
01081         return $tmpFiles;
01082     }
01083 
01084     public function getFileHttpUrl( array $params ) {
01085         if ( $this->swiftTempUrlKey != '' ||
01086             ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
01087         ) {
01088             list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
01089             if ( $srcRel === null ) {
01090                 return null; // invalid path
01091             }
01092 
01093             $auth = $this->getAuthentication();
01094             if ( !$auth ) {
01095                 return null;
01096             }
01097 
01098             $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
01099             $expires = time() + $ttl;
01100 
01101             if ( $this->swiftTempUrlKey != '' ) {
01102                 $url = $this->storageUrl( $auth, $srcCont, $srcRel );
01103                 // Swift wants the signature based on the unencoded object name
01104                 $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
01105                 $signature = hash_hmac( 'sha1',
01106                     "GET\n{$expires}\n{$contPath}/{$srcRel}",
01107                     $this->swiftTempUrlKey
01108                 );
01109 
01110                 return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
01111             } else { // give S3 API URL for rgw
01112                 // Path for signature starts with the bucket
01113                 $spath = '/' . rawurlencode( $srcCont ) . '/' .
01114                     str_replace( '%2F', '/', rawurlencode( $srcRel ) );
01115                 // Calculate the hash
01116                 $signature = base64_encode( hash_hmac(
01117                     'sha1',
01118                     "GET\n\n\n{$expires}\n{$spath}",
01119                     $this->rgwS3SecretKey,
01120                     true // raw
01121                 ) );
01122                 // See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
01123                 // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
01124                 return wfAppendQuery(
01125                     str_replace( '/swift/v1', '', // S3 API is the rgw default
01126                         $this->storageUrl( $auth ) . $spath ),
01127                     array(
01128                         'Signature' => $signature,
01129                         'Expires' => $expires,
01130                         'AWSAccessKeyId' => $this->rgwS3AccessKey )
01131                 );
01132             }
01133         }
01134 
01135         return null;
01136     }
01137 
01138     protected function directoriesAreVirtual() {
01139         return true;
01140     }
01141 
01150     protected function headersFromParams( array $params ) {
01151         $hdrs = array();
01152         if ( !empty( $params['latest'] ) ) {
01153             $hdrs['x-newest'] = 'true';
01154         }
01155 
01156         return $hdrs;
01157     }
01158 
01159     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01160         $statuses = array();
01161 
01162         $auth = $this->getAuthentication();
01163         if ( !$auth ) {
01164             foreach ( $fileOpHandles as $index => $fileOpHandle ) {
01165                 $statuses[$index] = Status::newFatal( 'backend-fail-connect', $this->name );
01166             }
01167 
01168             return $statuses;
01169         }
01170 
01171         // Split the HTTP requests into stages that can be done concurrently
01172         $httpReqsByStage = array(); // map of (stage => index => HTTP request)
01173         foreach ( $fileOpHandles as $index => $fileOpHandle ) {
01174             $reqs = $fileOpHandle->httpOp;
01175             // Convert the 'url' parameter to an actual URL using $auth
01176             foreach ( $reqs as $stage => &$req ) {
01177                 list( $container, $relPath ) = $req['url'];
01178                 $req['url'] = $this->storageUrl( $auth, $container, $relPath );
01179                 $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : array();
01180                 $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
01181                 $httpReqsByStage[$stage][$index] = $req;
01182             }
01183             $statuses[$index] = Status::newGood();
01184         }
01185 
01186         // Run all requests for the first stage, then the next, and so on
01187         $reqCount = count( $httpReqsByStage );
01188         for ( $stage = 0; $stage < $reqCount; ++$stage ) {
01189             $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
01190             foreach ( $httpReqs as $index => $httpReq ) {
01191                 // Run the callback for each request of this operation
01192                 $callback = $fileOpHandles[$index]->callback;
01193                 call_user_func_array( $callback, array( $httpReq, $statuses[$index] ) );
01194                 // On failure, abort all remaining requests for this operation
01195                 // (e.g. abort the DELETE request if the COPY request fails for a move)
01196                 if ( !$statuses[$index]->isOK() ) {
01197                     $stages = count( $fileOpHandles[$index]->httpOp );
01198                     for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
01199                         unset( $httpReqsByStage[$s][$index] );
01200                     }
01201                 }
01202             }
01203         }
01204 
01205         return $statuses;
01206     }
01207 
01230     protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
01231         $status = Status::newGood();
01232         $auth = $this->getAuthentication();
01233 
01234         if ( !$auth ) {
01235             $status->fatal( 'backend-fail-connect', $this->name );
01236 
01237             return $status;
01238         }
01239 
01240         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01241             'method' => 'POST',
01242             'url' => $this->storageUrl( $auth, $container ),
01243             'headers' => $this->authTokenHeaders( $auth ) + array(
01244                 'x-container-read' => implode( ',', $readGrps ),
01245                 'x-container-write' => implode( ',', $writeGrps )
01246             )
01247         ) );
01248 
01249         if ( $rcode != 204 && $rcode !== 202 ) {
01250             $status->fatal( 'backend-fail-internal', $this->name );
01251         }
01252 
01253         return $status;
01254     }
01255 
01264     protected function getContainerStat( $container, $bypassCache = false ) {
01265         $section = new ProfileSection( __METHOD__ . '-' . $this->name );
01266 
01267         if ( $bypassCache ) { // purge cache
01268             $this->containerStatCache->clear( $container );
01269         } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
01270             $this->primeContainerCache( array( $container ) ); // check persistent cache
01271         }
01272         if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
01273             $auth = $this->getAuthentication();
01274             if ( !$auth ) {
01275                 return null;
01276             }
01277 
01278             wfProfileIn( __METHOD__ . "-{$this->name}-miss" );
01279             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01280                 'method' => 'HEAD',
01281                 'url' => $this->storageUrl( $auth, $container ),
01282                 'headers' => $this->authTokenHeaders( $auth )
01283             ) );
01284             wfProfileOut( __METHOD__ . "-{$this->name}-miss" );
01285 
01286             if ( $rcode === 204 ) {
01287                 $stat = array(
01288                     'count' => $rhdrs['x-container-object-count'],
01289                     'bytes' => $rhdrs['x-container-bytes-used']
01290                 );
01291                 if ( $bypassCache ) {
01292                     return $stat;
01293                 } else {
01294                     $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
01295                     $this->setContainerCache( $container, $stat ); // update persistent cache
01296                 }
01297             } elseif ( $rcode === 404 ) {
01298                 return false;
01299             } else {
01300                 $this->onError( null, __METHOD__,
01301                     array( 'cont' => $container ), $rerr, $rcode, $rdesc );
01302 
01303                 return null;
01304             }
01305         }
01306 
01307         return $this->containerStatCache->get( $container, 'stat' );
01308     }
01309 
01317     protected function createContainer( $container, array $params ) {
01318         $status = Status::newGood();
01319 
01320         $auth = $this->getAuthentication();
01321         if ( !$auth ) {
01322             $status->fatal( 'backend-fail-connect', $this->name );
01323 
01324             return $status;
01325         }
01326 
01327         // @see SwiftFileBackend::setContainerAccess()
01328         if ( empty( $params['noAccess'] ) ) {
01329             $readGrps = array( '.r:*', $this->swiftUser ); // public
01330         } else {
01331             $readGrps = array( $this->swiftUser ); // private
01332         }
01333         $writeGrps = array( $this->swiftUser ); // sanity
01334 
01335         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01336             'method' => 'PUT',
01337             'url' => $this->storageUrl( $auth, $container ),
01338             'headers' => $this->authTokenHeaders( $auth ) + array(
01339                 'x-container-read' => implode( ',', $readGrps ),
01340                 'x-container-write' => implode( ',', $writeGrps )
01341             )
01342         ) );
01343 
01344         if ( $rcode === 201 ) { // new
01345             // good
01346         } elseif ( $rcode === 202 ) { // already there
01347             // this shouldn't really happen, but is OK
01348         } else {
01349             $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
01350         }
01351 
01352         return $status;
01353     }
01354 
01362     protected function deleteContainer( $container, array $params ) {
01363         $status = Status::newGood();
01364 
01365         $auth = $this->getAuthentication();
01366         if ( !$auth ) {
01367             $status->fatal( 'backend-fail-connect', $this->name );
01368 
01369             return $status;
01370         }
01371 
01372         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01373             'method' => 'DELETE',
01374             'url' => $this->storageUrl( $auth, $container ),
01375             'headers' => $this->authTokenHeaders( $auth )
01376         ) );
01377 
01378         if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
01379             $this->containerStatCache->clear( $container ); // purge
01380         } elseif ( $rcode === 404 ) { // not there
01381             // this shouldn't really happen, but is OK
01382         } elseif ( $rcode === 409 ) { // not empty
01383             $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
01384         } else {
01385             $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
01386         }
01387 
01388         return $status;
01389     }
01390 
01403     private function objectListing(
01404         $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
01405     ) {
01406         $status = Status::newGood();
01407 
01408         $auth = $this->getAuthentication();
01409         if ( !$auth ) {
01410             $status->fatal( 'backend-fail-connect', $this->name );
01411 
01412             return $status;
01413         }
01414 
01415         $query = array( 'limit' => $limit );
01416         if ( $type === 'info' ) {
01417             $query['format'] = 'json';
01418         }
01419         if ( $after !== null ) {
01420             $query['marker'] = $after;
01421         }
01422         if ( $prefix !== null ) {
01423             $query['prefix'] = $prefix;
01424         }
01425         if ( $delim !== null ) {
01426             $query['delimiter'] = $delim;
01427         }
01428 
01429         list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01430             'method' => 'GET',
01431             'url' => $this->storageUrl( $auth, $fullCont ),
01432             'query' => $query,
01433             'headers' => $this->authTokenHeaders( $auth )
01434         ) );
01435 
01436         $params = array( 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim );
01437         if ( $rcode === 200 ) { // good
01438             if ( $type === 'info' ) {
01439                 $status->value = FormatJson::decode( trim( $rbody ) );
01440             } else {
01441                 $status->value = explode( "\n", trim( $rbody ) );
01442             }
01443         } elseif ( $rcode === 204 ) {
01444             $status->value = array(); // empty container
01445         } elseif ( $rcode === 404 ) {
01446             $status->value = array(); // no container
01447         } else {
01448             $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
01449         }
01450 
01451         return $status;
01452     }
01453 
01454     protected function doPrimeContainerCache( array $containerInfo ) {
01455         foreach ( $containerInfo as $container => $info ) {
01456             $this->containerStatCache->set( $container, 'stat', $info );
01457         }
01458     }
01459 
01460     protected function doGetFileStatMulti( array $params ) {
01461         $stats = array();
01462 
01463         $auth = $this->getAuthentication();
01464 
01465         $reqs = array();
01466         foreach ( $params['srcs'] as $path ) {
01467             list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
01468             if ( $srcRel === null ) {
01469                 $stats[$path] = false;
01470                 continue; // invalid storage path
01471             } elseif ( !$auth ) {
01472                 $stats[$path] = null;
01473                 continue;
01474             }
01475 
01476             // (a) Check the container
01477             $cstat = $this->getContainerStat( $srcCont );
01478             if ( $cstat === false ) {
01479                 $stats[$path] = false;
01480                 continue; // ok, nothing to do
01481             } elseif ( !is_array( $cstat ) ) {
01482                 $stats[$path] = null;
01483                 continue;
01484             }
01485 
01486             $reqs[$path] = array(
01487                 'method'  => 'HEAD',
01488                 'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
01489                 'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
01490             );
01491         }
01492 
01493         $opts = array( 'maxConnsPerHost' => $params['concurrency'] );
01494         $reqs = $this->http->runMulti( $reqs, $opts );
01495 
01496         foreach ( $params['srcs'] as $path ) {
01497             if ( array_key_exists( $path, $stats ) ) {
01498                 continue; // some sort of failure above
01499             }
01500             // (b) Check the file
01501             list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
01502             if ( $rcode === 200 || $rcode === 204 ) {
01503                 // Update the object if it is missing some headers
01504                 $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
01505                 // Fetch all of the custom metadata headers
01506                 $metadata = array();
01507                 foreach ( $rhdrs as $name => $value ) {
01508                     if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
01509                         $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
01510                     }
01511                 }
01512                 // Fetch all of the custom raw HTTP headers
01513                 $headers = $this->sanitizeHdrs( array( 'headers' => $rhdrs ) );
01514                 $stat = array(
01515                     // Convert various random Swift dates to TS_MW
01516                     'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
01517                     // Empty objects actually return no content-length header in Ceph
01518                     'size'  => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
01519                     'sha1'  => $rhdrs[ 'x-object-meta-sha1base36'],
01520                     // Note: manifiest ETags are not an MD5 of the file
01521                     'md5'   => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
01522                     'xattr' => array( 'metadata' => $metadata, 'headers' => $headers )
01523                 );
01524                 if ( $this->isRGW ) {
01525                     $stat['latest'] = true; // strong consistency
01526                 }
01527             } elseif ( $rcode === 404 ) {
01528                 $stat = false;
01529             } else {
01530                 $stat = null;
01531                 $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
01532             }
01533             $stats[$path] = $stat;
01534         }
01535 
01536         return $stats;
01537     }
01538 
01542     protected function getAuthentication() {
01543         if ( $this->authErrorTimestamp !== null ) {
01544             if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
01545                 return null; // failed last attempt; don't bother
01546             } else { // actually retry this time
01547                 $this->authErrorTimestamp = null;
01548             }
01549         }
01550         // Session keys expire after a while, so we renew them periodically
01551         $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
01552         // Authenticate with proxy and get a session key...
01553         if ( !$this->authCreds || $reAuth ) {
01554             $this->authSessionTimestamp = 0;
01555             $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
01556             $creds = $this->srvCache->get( $cacheKey ); // credentials
01557             // Try to use the credential cache
01558             if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
01559                 $this->authCreds = $creds;
01560                 // Skew the timestamp for worst case to avoid using stale credentials
01561                 $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
01562             } else { // cache miss
01563                 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( array(
01564                     'method' => 'GET',
01565                     'url' => "{$this->swiftAuthUrl}/v1.0",
01566                     'headers' => array(
01567                         'x-auth-user' => $this->swiftUser,
01568                         'x-auth-key' => $this->swiftKey
01569                     )
01570                 ) );
01571 
01572                 if ( $rcode >= 200 && $rcode <= 299 ) { // OK
01573                     $this->authCreds = array(
01574                         'auth_token' => $rhdrs['x-auth-token'],
01575                         'storage_url' => $rhdrs['x-storage-url']
01576                     );
01577                     $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
01578                     $this->authSessionTimestamp = time();
01579                 } elseif ( $rcode === 401 ) {
01580                     $this->onError( null, __METHOD__, array(), "Authentication failed.", $rcode );
01581                     $this->authErrorTimestamp = time();
01582 
01583                     return null;
01584                 } else {
01585                     $this->onError( null, __METHOD__, array(), "HTTP return code: $rcode", $rcode );
01586                     $this->authErrorTimestamp = time();
01587 
01588                     return null;
01589                 }
01590             }
01591             // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
01592             if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
01593                 $this->isRGW = true; // take advantage of strong consistency
01594             }
01595         }
01596 
01597         return $this->authCreds;
01598     }
01599 
01606     protected function storageUrl( array $creds, $container = null, $object = null ) {
01607         $parts = array( $creds['storage_url'] );
01608         if ( strlen( $container ) ) {
01609             $parts[] = rawurlencode( $container );
01610         }
01611         if ( strlen( $object ) ) {
01612             $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
01613         }
01614 
01615         return implode( '/', $parts );
01616     }
01617 
01622     protected function authTokenHeaders( array $creds ) {
01623         return array( 'x-auth-token' => $creds['auth_token'] );
01624     }
01625 
01632     private function getCredsCacheKey( $username ) {
01633         return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
01634     }
01635 
01647     public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
01648         if ( $status instanceof Status ) {
01649             $status->fatal( 'backend-fail-internal', $this->name );
01650         }
01651         if ( $code == 401 ) { // possibly a stale token
01652             $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
01653         }
01654         wfDebugLog( 'SwiftBackend',
01655             "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
01656             ( $err ? ": $err" : "" )
01657         );
01658     }
01659 }
01660 
01664 class SwiftFileOpHandle extends FileBackendStoreOpHandle {
01666     public $httpOp;
01668     public $callback;
01669 
01675     public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
01676         $this->backend = $backend;
01677         $this->callback = $callback;
01678         $this->httpOp = $httpOp;
01679     }
01680 }
01681 
01689 abstract class SwiftFileBackendList implements Iterator {
01691     protected $bufferIter = array();
01692 
01694     protected $bufferAfter = null;
01695 
01697     protected $pos = 0;
01698 
01700     protected $params = array();
01701 
01703     protected $backend;
01704 
01706     protected $container;
01707 
01709     protected $dir;
01710 
01712     protected $suffixStart;
01713 
01714     const PAGE_SIZE = 9000; // file listing buffer size
01715 
01722     public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
01723         $this->backend = $backend;
01724         $this->container = $fullCont;
01725         $this->dir = $dir;
01726         if ( substr( $this->dir, -1 ) === '/' ) {
01727             $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
01728         }
01729         if ( $this->dir == '' ) { // whole container
01730             $this->suffixStart = 0;
01731         } else { // dir within container
01732             $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
01733         }
01734         $this->params = $params;
01735     }
01736 
01741     public function key() {
01742         return $this->pos;
01743     }
01744 
01748     public function next() {
01749         // Advance to the next file in the page
01750         next( $this->bufferIter );
01751         ++$this->pos;
01752         // Check if there are no files left in this page and
01753         // advance to the next page if this page was not empty.
01754         if ( !$this->valid() && count( $this->bufferIter ) ) {
01755             $this->bufferIter = $this->pageFromList(
01756                 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01757             ); // updates $this->bufferAfter
01758         }
01759     }
01760 
01764     public function rewind() {
01765         $this->pos = 0;
01766         $this->bufferAfter = null;
01767         $this->bufferIter = $this->pageFromList(
01768             $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01769         ); // updates $this->bufferAfter
01770     }
01771 
01776     public function valid() {
01777         if ( $this->bufferIter === null ) {
01778             return false; // some failure?
01779         } else {
01780             return ( current( $this->bufferIter ) !== false ); // no paths can have this value
01781         }
01782     }
01783 
01794     abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
01795 }
01796 
01800 class SwiftFileBackendDirList extends SwiftFileBackendList {
01805     public function current() {
01806         return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
01807     }
01808 
01809     protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01810         return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
01811     }
01812 }
01813 
01817 class SwiftFileBackendFileList extends SwiftFileBackendList {
01822     public function current() {
01823         list( $path, $stat ) = current( $this->bufferIter );
01824         $relPath = substr( $path, $this->suffixStart );
01825         if ( is_array( $stat ) ) {
01826             $storageDir = rtrim( $this->params['dir'], '/' );
01827             $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
01828         }
01829 
01830         return $relPath;
01831     }
01832 
01833     protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01834         return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
01835     }
01836 }