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