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