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