32use Psr\Log\LoggerInterface;
33use Shellbox\Command\BoxedCommand;
36use Wikimedia\AtEase\AtEase;
44use Wikimedia\RequestTimeout\TimeoutException;
56 private const DEFAULT_HTTP_OPTIONS = [
'httpVersion' =>
'v1.1' ];
57 private const AUTH_FAILURE_ERROR =
'Could not connect due to prior authentication failure';
148 parent::__construct( $config );
150 $this->swiftAuthUrl = $config[
'swiftAuthUrl'];
151 $this->swiftUser = $config[
'swiftUser'];
152 $this->swiftKey = $config[
'swiftKey'];
154 $this->authTTL = $config[
'swiftAuthTTL'] ?? 15 * 60;
155 $this->swiftTempUrlKey = $config[
'swiftTempUrlKey'] ??
'';
156 $this->canShellboxGetTempUrl = $config[
'canShellboxGetTempUrl'] ??
false;
157 $this->shellboxIpRange = $config[
'shellboxIpRange'] ??
null;
158 $this->swiftStorageUrl = $config[
'swiftStorageUrl'] ??
null;
159 $this->shardViaHashLevels = $config[
'shardViaHashLevels'] ??
'';
160 $this->rgwS3AccessKey = $config[
'rgwS3AccessKey'] ??
'';
161 $this->rgwS3SecretKey = $config[
'rgwS3SecretKey'] ??
'';
165 foreach ( [
'connTimeout',
'reqTimeout' ] as $optionName ) {
166 if ( isset( $config[$optionName] ) ) {
167 $httpOptions[$optionName] = $config[$optionName];
171 $this->http->setLogger( $this->logger );
174 if ( isset( $config[
'wanCache'] ) && $config[
'wanCache'] instanceof
WANObjectCache ) {
175 $this->memCache = $config[
'wanCache'];
178 $this->containerStatCache =
new MapCacheLRU( 300 );
180 if ( !empty( $config[
'cacheAuthInfo'] ) && isset( $config[
'srvCache'] ) ) {
181 $this->srvCache = $config[
'srvCache'];
185 $this->readUsers = $config[
'readUsers'] ?? [];
186 $this->writeUsers = $config[
'writeUsers'] ?? [];
187 $this->secureReadUsers = $config[
'secureReadUsers'] ?? [];
188 $this->secureWriteUsers = $config[
'secureWriteUsers'] ?? [];
193 $this->maxFileSize = 5 * 1024 * 1024 * 1024;
198 $this->http->setLogger(
$logger );
203 self::ATTR_UNICODE_PATHS |
210 if ( !mb_check_encoding( $relStoragePath,
'UTF-8' ) ) {
212 } elseif ( strlen( rawurlencode( $relStoragePath ) ) > 1024 ) {
216 return $relStoragePath;
221 if ( $rel ===
null ) {
238 $contentHeaders = [];
240 foreach ( $headers as
$name => $value ) {
242 if (
$name ===
'x-delete-at' && is_numeric( $value ) ) {
244 $contentHeaders[
$name] = $value;
245 } elseif (
$name ===
'x-delete-after' && is_numeric( $value ) ) {
247 $contentHeaders[
$name] = $value;
248 } elseif ( preg_match(
'/^(x-)?content-(?!length$)/',
$name ) ) {
250 $contentHeaders[
$name] = $value;
251 } elseif (
$name ===
'content-type' && strlen( $value ) ) {
253 $contentHeaders[
$name] = $value;
257 if ( isset( $contentHeaders[
'content-disposition'] ) ) {
260 $offset = $maxLength - strlen( $contentHeaders[
'content-disposition'] );
262 $pos = strrpos( $contentHeaders[
'content-disposition'],
';', $offset );
263 $contentHeaders[
'content-disposition'] = $pos ===
false
265 : trim( substr( $contentHeaders[
'content-disposition'], 0, $pos ) );
269 return $contentHeaders;
278 $metadataHeaders = [];
279 foreach ( $headers as
$name => $value ) {
281 if ( strpos(
$name,
'x-object-meta-' ) === 0 ) {
282 $metadataHeaders[
$name] = $value;
286 return $metadataHeaders;
295 $prefixLen = strlen(
'x-object-meta-' );
299 $metadata[substr(
$name, $prefixLen )] = $value;
309 if ( $dstRel ===
null ) {
310 $status->fatal(
'backend-fail-invalidpath',
$params[
'dst'] );
318 $mutableHeaders[
'content-type']
323 'container' => $dstCont,
324 'relPath' => $dstRel,
325 'headers' => array_merge(
328 'etag' => md5(
$params[
'content'] ),
329 'content-length' => strlen(
$params[
'content'] ),
330 'x-object-meta-sha1base36' =>
337 $method = __METHOD__;
338 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
339 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
340 if ( $rcode === 201 || $rcode === 202 ) {
342 } elseif ( $rcode === 412 ) {
343 $status->fatal(
'backend-fail-contenttype',
$params[
'dst'] );
345 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
348 return SwiftFileOpHandle::CONTINUE_IF_OK;
352 if ( !empty(
$params[
'async'] ) ) {
353 $status->value = $opHandle;
365 if ( $dstRel ===
null ) {
366 $status->fatal(
'backend-fail-invalidpath',
$params[
'dst'] );
374 AtEase::suppressWarnings();
375 $srcHandle = fopen(
$params[
'src'],
'rb' );
376 AtEase::restoreWarnings();
377 if ( $srcHandle ===
false ) {
378 $status->fatal(
'backend-fail-notexists',
$params[
'src'] );
384 $srcSize = fstat( $srcHandle )[
'size'];
385 $md5Context = hash_init(
'md5' );
386 $sha1Context = hash_init(
'sha1' );
388 while ( !feof( $srcHandle ) ) {
389 $buffer = (string)fread( $srcHandle, 131_072 );
390 hash_update( $md5Context, $buffer );
391 hash_update( $sha1Context, $buffer );
392 $hashDigestSize += strlen( $buffer );
395 rewind( $srcHandle );
397 if ( $hashDigestSize !== $srcSize ) {
398 $status->fatal(
'backend-fail-hash',
$params[
'src'] );
406 $mutableHeaders[
'content-type']
411 'container' => $dstCont,
412 'relPath' => $dstRel,
413 'headers' => array_merge(
416 'content-length' => $srcSize,
417 'etag' => hash_final( $md5Context ),
418 'x-object-meta-sha1base36' =>
419 \
Wikimedia\base_convert( hash_final( $sha1Context ), 16, 36, 31 )
425 $method = __METHOD__;
426 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
427 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
428 if ( $rcode === 201 || $rcode === 202 ) {
430 } elseif ( $rcode === 412 ) {
431 $status->fatal(
'backend-fail-contenttype',
$params[
'dst'] );
433 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
436 return SwiftFileOpHandle::CONTINUE_IF_OK;
440 $opHandle->resourcesToClose[] = $srcHandle;
442 if ( !empty(
$params[
'async'] ) ) {
443 $status->value = $opHandle;
455 if ( $srcRel ===
null ) {
456 $status->fatal(
'backend-fail-invalidpath',
$params[
'src'] );
462 if ( $dstRel ===
null ) {
463 $status->fatal(
'backend-fail-invalidpath',
$params[
'dst'] );
470 'container' => $dstCont,
471 'relPath' => $dstRel,
472 'headers' => array_merge(
475 'x-copy-from' =>
'/' . rawurlencode( $srcCont ) .
'/' .
476 str_replace(
"%2F",
"/", rawurlencode( $srcRel ) )
481 $method = __METHOD__;
482 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
483 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
484 if ( $rcode === 201 ) {
486 } elseif ( $rcode === 404 ) {
487 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
488 $status->fatal(
'backend-fail-copy',
$params[
'src'],
$params[
'dst'] );
491 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
494 return SwiftFileOpHandle::CONTINUE_IF_OK;
498 if ( !empty(
$params[
'async'] ) ) {
499 $status->value = $opHandle;
511 if ( $srcRel ===
null ) {
512 $status->fatal(
'backend-fail-invalidpath',
$params[
'src'] );
518 if ( $dstRel ===
null ) {
519 $status->fatal(
'backend-fail-invalidpath',
$params[
'dst'] );
526 'container' => $dstCont,
527 'relPath' => $dstRel,
528 'headers' => array_merge(
531 'x-copy-from' =>
'/' . rawurlencode( $srcCont ) .
'/' .
532 str_replace(
"%2F",
"/", rawurlencode( $srcRel ) )
536 if (
"{$srcCont}/{$srcRel}" !==
"{$dstCont}/{$dstRel}" ) {
538 'method' =>
'DELETE',
539 'container' => $srcCont,
540 'relPath' => $srcRel,
545 $method = __METHOD__;
546 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
547 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
548 if ( $request[
'method'] ===
'PUT' && $rcode === 201 ) {
550 } elseif ( $request[
'method'] ===
'DELETE' && $rcode === 204 ) {
552 } elseif ( $rcode === 404 ) {
553 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
554 $status->fatal(
'backend-fail-move',
$params[
'src'],
$params[
'dst'] );
557 return SwiftFileOpHandle::CONTINUE_NO;
560 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
563 return SwiftFileOpHandle::CONTINUE_IF_OK;
567 if ( !empty(
$params[
'async'] ) ) {
568 $status->value = $opHandle;
580 if ( $srcRel ===
null ) {
581 $status->fatal(
'backend-fail-invalidpath',
$params[
'src'] );
587 'method' =>
'DELETE',
588 'container' => $srcCont,
589 'relPath' => $srcRel,
593 $method = __METHOD__;
594 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
595 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
596 if ( $rcode === 204 ) {
598 } elseif ( $rcode === 404 ) {
599 if ( empty(
$params[
'ignoreMissingSource'] ) ) {
600 $status->fatal(
'backend-fail-delete',
$params[
'src'] );
603 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
606 return SwiftFileOpHandle::CONTINUE_IF_OK;
610 if ( !empty(
$params[
'async'] ) ) {
611 $status->value = $opHandle;
623 if ( $srcRel ===
null ) {
624 $status->fatal(
'backend-fail-invalidpath',
$params[
'src'] );
630 $stat = $this->
getFileStat( [
'src' => $params[
'src'],
'latest' => 1 ] );
631 if ( $stat && !isset( $stat[
'xattr'] ) ) {
632 $stat = $this->
doGetFileStat( [
'src' => $params[
'src'],
'latest' => 1 ] );
635 $status->fatal(
'backend-fail-describe',
$params[
'src'] );
643 $oldMetadataHeaders = [];
644 foreach ( $stat[
'xattr'][
'metadata'] as
$name => $value ) {
645 $oldMetadataHeaders[
"x-object-meta-$name"] = $value;
648 $oldContentHeaders = $stat[
'xattr'][
'headers'];
652 'container' => $srcCont,
653 'relPath' => $srcRel,
654 'headers' => $oldMetadataHeaders + $newContentHeaders + $oldContentHeaders
657 $method = __METHOD__;
658 $handler =
function ( array $request,
StatusValue $status ) use ( $method,
$params ) {
659 [ $rcode, $rdesc, , $rbody, $rerr ] = $request[
'response'];
660 if ( $rcode === 202 ) {
662 } elseif ( $rcode === 404 ) {
663 $status->fatal(
'backend-fail-describe',
$params[
'src'] );
665 $this->
onError( $status, $method,
$params, $rerr, $rcode, $rdesc, $rbody );
670 if ( !empty(
$params[
'async'] ) ) {
671 $status->value = $opHandle;
687 if ( is_array( $stat ) ) {
689 } elseif ( $stat === self::RES_ERROR ) {
690 $status->fatal(
'backend-fail-internal', $this->name );
691 $this->logger->error( __METHOD__ .
': cannot get container stat' );
703 if ( empty(
$params[
'noAccess'] ) ) {
708 if ( is_array( $stat ) ) {
709 $readUsers = array_merge( $this->secureReadUsers, [ $this->swiftUser ] );
710 $writeUsers = array_merge( $this->secureWriteUsers, [ $this->swiftUser ] );
717 } elseif ( $stat === self::RES_ABSENT ) {
718 $status->fatal(
'backend-fail-usable',
$params[
'dir'] );
720 $status->fatal(
'backend-fail-internal', $this->name );
721 $this->logger->error( __METHOD__ .
': cannot get container stat' );
729 if ( empty(
$params[
'access'] ) ) {
734 if ( is_array( $stat ) ) {
735 $readUsers = array_merge( $this->readUsers, [ $this->swiftUser,
'.r:*' ] );
736 if ( !empty(
$params[
'listing'] ) ) {
739 $writeUsers = array_merge( $this->writeUsers, [ $this->swiftUser ] );
747 } elseif ( $stat === self::RES_ABSENT ) {
748 $status->fatal(
'backend-fail-usable',
$params[
'dir'] );
750 $status->fatal(
'backend-fail-internal', $this->name );
751 $this->logger->error( __METHOD__ .
': cannot get container stat' );
767 if ( $stat === self::RES_ABSENT ) {
769 } elseif ( $stat === self::RES_ERROR ) {
770 $status->fatal(
'backend-fail-internal', $this->name );
771 $this->logger->error( __METHOD__ .
': cannot get container stat' );
772 } elseif ( is_array( $stat ) && $stat[
'count'] == 0 ) {
786 return reset( $stats );
803 return $timestamp->getTimestamp( $format );
804 }
catch ( TimeoutException $e ) {
806 }
catch ( Exception $e ) {
819 if ( isset( $objHdrs[
'x-object-meta-sha1base36'] ) ) {
825 $this->logger->error( __METHOD__ .
": {path} was not stored with SHA-1 metadata.",
826 [
'path' =>
$path ] );
828 $objHdrs[
'x-object-meta-sha1base36'] =
false;
838 if ( $status->isOK() ) {
841 $hash = $tmpFile->getSha1Base36();
842 if ( $hash !==
false ) {
843 $objHdrs[
'x-object-meta-sha1base36'] = $hash;
845 $postHeaders[
'x-object-meta-sha1base36'] = $hash;
847 [ $rcode ] = $this->requestWithAuth( [
849 'container' => $srcCont,
850 'relPath' => $srcRel,
851 'headers' => $postHeaders
853 if ( $rcode >= 200 && $rcode <= 299 ) {
862 $this->logger->error( __METHOD__ .
': unable to set SHA-1 metadata for {path}',
863 [
'path' =>
$path ] );
869 $ep = array_diff_key(
$params, [
'srcs' => 1 ] );
875 $contents = array_fill_keys(
$params[
'srcs'], self::RES_ERROR );
878 if ( $srcRel ===
null ) {
882 $handle = fopen(
'php://temp',
'wb' );
886 'container' => $srcCont,
887 'relPath' => $srcRel,
894 $reqs = $this->requestMultiWithAuth(
896 [
'maxConnsPerHost' =>
$params[
'concurrency'] ]
898 foreach ( $reqs as
$path => $op ) {
899 [ $rcode, $rdesc, $rhdrs, $rbody, $rerr ] = $op[
'response'];
900 if ( $rcode >= 200 && $rcode <= 299 ) {
901 rewind( $op[
'stream'] );
902 $content = (string)stream_get_contents( $op[
'stream'] );
903 $size = strlen( $content );
905 if ( $size === (
int)$rhdrs[
'content-length'] ) {
906 $contents[
$path] = $content;
908 $contents[
$path] = self::RES_ERROR;
909 $rerr =
"Got {$size}/{$rhdrs['content-length']} bytes";
910 $this->
onError(
null, __METHOD__,
911 [
'src' =>
$path ] + $ep, $rerr, $rcode, $rdesc );
913 } elseif ( $rcode === 404 ) {
914 $contents[
$path] = self::RES_ABSENT;
916 $contents[
$path] = self::RES_ERROR;
917 $this->
onError(
null, __METHOD__,
918 [
'src' =>
$path ] + $ep, $rerr, $rcode, $rdesc, $rbody );
920 fclose( $op[
'stream'] );
927 $prefix = ( $dir ==
'' ) ?
null :
"{$dir}/";
928 $status = $this->objectListing( $fullCont,
'names', 1,
null, $prefix );
929 if ( $status->isOK() ) {
930 return ( count( $status->value ) ) > 0;
933 return self::RES_ERROR;
971 if ( $after === INF ) {
978 $prefix = ( $dir ==
'' ) ?
null :
"{$dir}/";
980 if ( !empty(
$params[
'topOnly'] ) ) {
981 $status = $this->objectListing( $fullCont,
'names', $limit, $after, $prefix,
'/' );
982 if ( !$status->isOK() ) {
985 $objects = $status->value;
987 foreach ( $objects as $object ) {
988 if ( substr( $object, -1 ) ===
'/' ) {
994 $getParentDir =
static function (
$path ) {
995 return (
$path !==
null && strpos(
$path,
'/' ) !== false ) ? dirname(
$path ) :
false;
999 $lastDir = $getParentDir( $after );
1000 $status = $this->objectListing( $fullCont,
'names', $limit, $after, $prefix );
1002 if ( !$status->isOK() ) {
1006 $objects = $status->value;
1009 foreach ( $objects as $object ) {
1010 $objectDir = $getParentDir( $object );
1012 if ( $objectDir !==
false && $objectDir !== $dir ) {
1017 if ( strcmp( $objectDir, $lastDir ) > 0 ) {
1020 $dirs[] =
"{$pDir}/";
1021 $pDir = $getParentDir( $pDir );
1022 }
while ( $pDir !==
false
1023 && strcmp( $pDir, $lastDir ) > 0
1024 && strlen( $pDir ) > strlen( $dir )
1027 $lastDir = $objectDir;
1032 if ( count( $objects ) < $limit ) {
1035 $after = end( $objects );
1054 if ( $after === INF ) {
1061 $prefix = ( $dir ==
'' ) ?
null :
"{$dir}/";
1064 if ( !empty(
$params[
'topOnly'] ) ) {
1065 if ( !empty(
$params[
'adviseStat'] ) ) {
1066 $status = $this->objectListing( $fullCont,
'info', $limit, $after, $prefix,
'/' );
1068 $status = $this->objectListing( $fullCont,
'names', $limit, $after, $prefix,
'/' );
1072 if ( !empty(
$params[
'adviseStat'] ) ) {
1073 $status = $this->objectListing( $fullCont,
'info', $limit, $after, $prefix );
1075 $status = $this->objectListing( $fullCont,
'names', $limit, $after, $prefix );
1080 if ( !$status->isOK() ) {
1084 $objects = $status->value;
1085 $files = $this->buildFileObjectListing( $objects );
1088 if ( count( $objects ) < $limit ) {
1091 $after = end( $objects );
1092 $after = is_object( $after ) ? $after->name : $after;
1105 private function buildFileObjectListing( array $objects ) {
1107 foreach ( $objects as $object ) {
1108 if ( is_object( $object ) ) {
1109 if ( isset( $object->subdir ) || !isset( $object->name ) ) {
1115 'size' => (int)$object->bytes,
1118 'md5' => ctype_xdigit( $object->hash ) ? $object->hash :
null,
1121 $names[] = [ $object->name, $stat ];
1122 } elseif ( substr( $object, -1 ) !==
'/' ) {
1124 $names[] = [ $object, null ];
1138 $this->cheapCache->setField(
$path,
'stat', $val );
1144 if ( is_array( $stat ) && !isset( $stat[
'xattr'] ) ) {
1149 if ( is_array( $stat ) ) {
1150 return $stat[
'xattr'];
1153 return $stat === self::RES_ERROR ? self::RES_ERROR : self::RES_ABSENT;
1159 $params[
'requireSHA1'] =
true;
1162 if ( is_array( $stat ) ) {
1163 return $stat[
'sha1'];
1166 return $stat === self::RES_ERROR ? self::RES_ERROR : self::RES_ABSENT;
1175 if ( $srcRel ===
null ) {
1177 $status->fatal(
'backend-fail-invalidpath',
$params[
'src'] );
1184 $status->fatal(
'backend-fail-stream',
$params[
'src'] );
1193 $status->fatal(
'backend-fail-stream',
$params[
'src'] );
1199 if ( empty(
$params[
'headless'] ) ) {
1201 $this->
header( $header );
1205 if ( empty(
$params[
'allowOB'] ) ) {
1210 $handle = fopen(
'php://output',
'wb' );
1211 [ $rcode, $rdesc, , $rbody, $rerr ] = $this->requestWithAuth( [
1213 'container' => $srcCont,
1214 'relPath' => $srcRel,
1216 'stream' => $handle,
1217 'flags' => [
'relayResponseHeaders' => empty(
$params[
'headless'] ) ]
1220 if ( $rcode >= 200 && $rcode <= 299 ) {
1222 } elseif ( $rcode === 404 ) {
1223 $status->fatal(
'backend-fail-stream',
$params[
'src'] );
1230 $this->
onError( $status, __METHOD__,
$params, $rerr, $rcode, $rdesc, $rbody );
1237 $ep = array_diff_key(
$params, [
'srcs' => 1 ] );
1243 $tmpFiles = array_fill_keys(
$params[
'srcs'], self::RES_ERROR );
1246 if ( $srcRel ===
null ) {
1252 $tmpFile = $this->tmpFileFactory->newTempFSFile(
'localcopy_', $ext );
1253 $handle = $tmpFile ? fopen( $tmpFile->getPath(),
'wb' ) :
false;
1257 'container' => $srcCont,
1258 'relPath' => $srcRel,
1260 'stream' => $handle,
1262 $tmpFiles[
$path] = $tmpFile;
1267 $latest = ( $this->isRGW || !empty(
$params[
'latest'] ) );
1269 $reqs = $this->requestMultiWithAuth(
1271 [
'maxConnsPerHost' =>
$params[
'concurrency'] ]
1273 foreach ( $reqs as
$path => $op ) {
1274 [ $rcode, $rdesc, $rhdrs, $rbody, $rerr ] = $op[
'response'];
1275 fclose( $op[
'stream'] );
1276 if ( $rcode >= 200 && $rcode <= 299 ) {
1278 $tmpFile = $tmpFiles[
$path];
1280 $size = $tmpFile->getSize();
1281 if ( $size !== (
int)$rhdrs[
'content-length'] ) {
1282 $tmpFiles[
$path] = self::RES_ERROR;
1283 $rerr =
"Got {$size}/{$rhdrs['content-length']} bytes";
1284 $this->
onError(
null, __METHOD__,
1285 [
'src' =>
$path ] + $ep, $rerr, $rcode, $rdesc );
1289 $stat[
'latest'] = $latest;
1290 $this->cheapCache->setField(
$path,
'stat', $stat );
1291 } elseif ( $rcode === 404 ) {
1292 $tmpFiles[
$path] = self::RES_ABSENT;
1293 $this->cheapCache->setField(
1296 $latest ? self::ABSENT_LATEST : self::ABSENT_NORMAL
1299 $tmpFiles[
$path] = self::RES_ERROR;
1300 $this->
onError(
null, __METHOD__,
1301 [
'src' =>
$path ] + $ep, $rerr, $rcode, $rdesc, $rbody );
1311 if ( $this->canShellboxGetTempUrl ) {
1312 $urlParams = [
'src' =>
$params[
'src'] ];
1313 if ( $this->shellboxIpRange !==
null ) {
1318 $command->inputFileFromUrl( $boxedName,
$url );
1322 return parent::addShellboxInputFile( $command, $boxedName,
$params );
1326 if ( $this->swiftTempUrlKey ==
'' &&
1327 ( $this->rgwS3AccessKey ==
'' || $this->rgwS3SecretKey !=
'' )
1329 $this->logger->debug(
"Can't get Swift file URL: no key available" );
1330 return self::TEMPURL_ERROR;
1334 if ( $srcRel ===
null ) {
1335 $this->logger->debug(
"Can't get Swift file URL: can't resolve path" );
1336 return self::TEMPURL_ERROR;
1341 $this->logger->debug(
"Can't get Swift file URL: authentication failed" );
1342 return self::TEMPURL_ERROR;
1345 $method =
$params[
'method'] ??
'GET';
1346 $ttl =
$params[
'ttl'] ?? 86400;
1347 $expires = time() + $ttl;
1349 if ( $this->swiftTempUrlKey !=
'' ) {
1352 $contPath = parse_url( $this->
storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1356 "{$contPath}/{$srcRel}"
1359 'temp_url_expires' => $expires,
1361 if ( isset(
$params[
'ipRange'] ) ) {
1362 array_unshift( $messageParts,
"ip={$params['ipRange']}" );
1363 $query[
'temp_url_ip_range'] =
$params[
'ipRange'];
1366 $signature = hash_hmac(
'sha1',
1367 implode(
"\n", $messageParts ),
1368 $this->swiftTempUrlKey
1370 $query = [
'temp_url_sig' => $signature ] + $query;
1372 return $url .
'?' . http_build_query( $query );
1375 $spath =
'/' . rawurlencode( $srcCont ) .
'/' .
1376 str_replace(
'%2F',
'/', rawurlencode( $srcRel ) );
1378 $signature = base64_encode( hash_hmac(
1380 "{$method}\n\n\n{$expires}\n{$spath}",
1381 $this->rgwS3SecretKey,
1387 return str_replace(
'/swift/v1',
'', $this->
storageUrl( $auth ) . $spath ) .
1390 'Signature' => $signature,
1391 'Expires' => $expires,
1392 'AWSAccessKeyId' => $this->rgwS3AccessKey
1411 if ( !empty(
$params[
'latest'] ) ) {
1412 $hdrs[
'x-newest'] =
'true';
1420 '@phan-var SwiftFileOpHandle[] $fileOpHandles';
1426 $httpReqsByStage = [];
1427 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1428 $reqs = $fileOpHandle->httpOp;
1429 foreach ( $reqs as $stage => $req ) {
1430 $httpReqsByStage[$stage][$index] = $req;
1436 $reqCount = count( $httpReqsByStage );
1437 for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1438 $httpReqs = $this->requestMultiWithAuth( $httpReqsByStage[$stage] );
1439 foreach ( $httpReqs as $index => $httpReq ) {
1441 $fileOpHandle = $fileOpHandles[$index];
1443 $status = $statuses[$index];
1444 ( $fileOpHandle->callback )( $httpReq, $status );
1449 $fileOpHandle->state === $fileOpHandle::CONTINUE_NO
1451 $stages = count( $fileOpHandle->httpOp );
1452 for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1453 unset( $httpReqsByStage[$s][$index] );
1487 [ $rcode, , , , ] = $this->requestWithAuth( [
1489 'container' => $container,
1491 'x-container-read' => implode(
',',
$readUsers ),
1492 'x-container-write' => implode(
',',
$writeUsers )
1496 if ( $rcode != 204 && $rcode !== 202 ) {
1497 $status->fatal(
'backend-fail-internal', $this->name );
1498 $this->logger->error( __METHOD__ .
': unexpected rcode value ({rcode})',
1499 [
'rcode' => $rcode ] );
1517 if ( $bypassCache ) {
1518 $this->containerStatCache->clear( $container );
1519 } elseif ( !$this->containerStatCache->hasField( $container,
'stat' ) ) {
1522 if ( !$this->containerStatCache->hasField( $container,
'stat' ) ) {
1523 [ $rcode, $rdesc, $rhdrs, $rbody, $rerr ] = $this->requestWithAuth( [
1525 'container' => $container
1528 if ( $rcode === 204 ) {
1530 'count' => $rhdrs[
'x-container-object-count'],
1531 'bytes' => $rhdrs[
'x-container-bytes-used']
1533 if ( $bypassCache ) {
1536 $this->containerStatCache->setField( $container,
'stat', $stat );
1539 } elseif ( $rcode === 404 ) {
1540 return self::RES_ABSENT;
1542 $this->
onError(
null, __METHOD__,
1543 [
'cont' => $container ], $rerr, $rcode, $rdesc, $rbody );
1545 return self::RES_ERROR;
1549 return $this->containerStatCache->getField( $container,
'stat' );
1563 if ( empty(
$params[
'noAccess'] ) ) {
1565 $readUsers = array_merge( $this->readUsers, [
'.r:*', $this->swiftUser ] );
1566 if ( empty(
$params[
'noListing'] ) ) {
1569 $writeUsers = array_merge( $this->writeUsers, [ $this->swiftUser ] );
1572 $readUsers = array_merge( $this->secureReadUsers, [ $this->swiftUser ] );
1573 $writeUsers = array_merge( $this->secureWriteUsers, [ $this->swiftUser ] );
1576 [ $rcode, $rdesc, , $rbody, $rerr ] = $this->requestWithAuth( [
1578 'container' => $container,
1580 'x-container-read' => implode(
',',
$readUsers ),
1581 'x-container-write' => implode(
',',
$writeUsers )
1585 if ( $rcode === 201 ) {
1587 } elseif ( $rcode === 202 ) {
1590 $this->
onError( $status, __METHOD__,
$params, $rerr, $rcode, $rdesc, $rbody );
1606 [ $rcode, $rdesc, , $rbody, $rerr ] = $this->requestWithAuth( [
1607 'method' =>
'DELETE',
1608 'container' => $container
1611 if ( $rcode >= 200 && $rcode <= 299 ) {
1612 $this->containerStatCache->clear( $container );
1613 } elseif ( $rcode === 404 ) {
1615 } elseif ( $rcode === 409 ) {
1616 $this->
onError( $status, __METHOD__,
$params, $rerr, $rcode, $rdesc );
1618 $this->
onError( $status, __METHOD__,
$params, $rerr, $rcode, $rdesc, $rbody );
1636 private function objectListing(
1637 $fullCont, $type, $limit, $after =
null, $prefix =
null, $delim =
null
1641 $query = [
'limit' => $limit ];
1642 if ( $type ===
'info' ) {
1643 $query[
'format'] =
'json';
1645 if ( $after !==
null ) {
1646 $query[
'marker'] = $after;
1648 if ( $prefix !==
null ) {
1649 $query[
'prefix'] = $prefix;
1651 if ( $delim !==
null ) {
1652 $query[
'delimiter'] = $delim;
1655 [ $rcode, $rdesc, , $rbody, $rerr ] = $this->requestWithAuth( [
1657 'container' => $fullCont,
1661 $params = [
'cont' => $fullCont,
'prefix' => $prefix,
'delim' => $delim ];
1662 if ( $rcode === 200 ) {
1663 if ( $type ===
'info' ) {
1664 $status->value = FormatJson::decode( trim( $rbody ) );
1666 $status->value = explode(
"\n", trim( $rbody ) );
1668 } elseif ( $rcode === 204 ) {
1669 $status->value = [];
1670 } elseif ( $rcode === 404 ) {
1671 $status->value = [];
1673 $this->
onError( $status, __METHOD__,
$params, $rerr, $rcode, $rdesc, $rbody );
1680 foreach ( $containerInfo as $container => $info ) {
1681 $this->containerStatCache->setField( $container,
'stat', $info );
1692 if ( $srcRel ===
null ) {
1694 $stats[
$path] = self::RES_ERROR;
1699 if ( $cstat === self::RES_ABSENT ) {
1700 $stats[
$path] = self::RES_ABSENT;
1702 } elseif ( $cstat === self::RES_ERROR ) {
1703 $stats[
$path] = self::RES_ERROR;
1709 'container' => $srcCont,
1710 'relPath' => $srcRel,
1716 $reqs = $this->requestMultiWithAuth(
1718 [
'maxConnsPerHost' =>
$params[
'concurrency'] ]
1720 foreach ( $reqs as
$path => $op ) {
1721 [ $rcode, $rdesc, $rhdrs, $rbody, $rerr ] = $op[
'response'];
1722 if ( $rcode === 200 || $rcode === 204 ) {
1724 if ( !empty(
$params[
'requireSHA1'] ) ) {
1729 if ( $this->isRGW ) {
1730 $stat[
'latest'] =
true;
1732 } elseif ( $rcode === 404 ) {
1733 $stat = self::RES_ABSENT;
1735 $stat = self::RES_ERROR;
1736 $this->
onError(
null, __METHOD__,
$params, $rerr, $rcode, $rdesc, $rbody );
1738 $stats[
$path] = $stat;
1758 'size' => isset( $rhdrs[
'content-length'] ) ? (int)$rhdrs[
'content-length'] : 0,
1759 'sha1' => $metadata[
'sha1base36'] ??
null,
1761 'md5' => ctype_xdigit( $rhdrs[
'etag'] ) ? $rhdrs[
'etag'] :
null,
1762 'xattr' => [
'metadata' => $metadata,
'headers' => $headers ]
1772 if ( $this->authErrorTimestamp !==
null ) {
1774 if ( $interval < 60 ) {
1775 $this->logger->debug(
1776 'rejecting request since auth failure occurred {interval} seconds ago',
1777 [
'interval' => $interval ]
1781 $this->authErrorTimestamp =
null;
1785 if ( !$this->authCreds ) {
1786 $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1787 $creds = $this->srvCache->get( $cacheKey );
1789 if ( isset( $creds[
'auth_token'] )
1790 && isset( $creds[
'storage_url'] )
1791 && isset( $creds[
'expiry_time'] )
1792 && $creds[
'expiry_time'] > time()
1794 $this->setAuthCreds( $creds );
1796 $this->refreshAuthentication();
1808 private function setAuthCreds( ?array $creds ) {
1809 $this->logger->debug(
'Using auth token with expiry_time={expiry_time}',
1811 'expiry_time' => isset( $creds[
'expiry_time'] )
1812 ? gmdate(
'c', $creds[
'expiry_time'] ) :
'null'
1815 $this->authCreds = $creds;
1817 if ( $creds && str_ends_with( $creds[
'storage_url'],
'/v1' ) ) {
1818 $this->isRGW =
true;
1827 private function refreshAuthentication() {
1828 [ $rcode, , $rhdrs, $rbody, ] = $this->http->run( [
1830 'url' =>
"{$this->swiftAuthUrl}/v1.0",
1832 'x-auth-user' => $this->swiftUser,
1833 'x-auth-key' => $this->swiftKey
1835 ], self::DEFAULT_HTTP_OPTIONS );
1837 if ( $rcode >= 200 && $rcode <= 299 ) {
1838 if ( isset( $rhdrs[
'x-auth-token-expires'] ) ) {
1839 $ttl = intval( $rhdrs[
'x-auth-token-expires'] );
1843 $expiryTime = time() + $ttl;
1845 'auth_token' => $rhdrs[
'x-auth-token'],
1846 'storage_url' => $this->swiftStorageUrl ?? $rhdrs[
'x-storage-url'],
1847 'expiry_time' => $expiryTime,
1849 $this->srvCache->set( $this->getCredsCacheKey( $this->swiftUser ), $creds, $expiryTime );
1850 } elseif ( $rcode === 401 ) {
1851 $this->
onError(
null, __METHOD__, [],
"Authentication failed.", $rcode );
1852 $this->authErrorTimestamp = time();
1855 $this->
onError(
null, __METHOD__, [],
"HTTP return code: $rcode", $rcode, $rbody );
1856 $this->authErrorTimestamp = time();
1859 $this->setAuthCreds( $creds );
1869 protected function storageUrl( array $creds, $container =
null, $object =
null ) {
1870 $parts = [ $creds[
'storage_url'] ];
1871 if ( strlen( $container ??
'' ) ) {
1872 $parts[] = rawurlencode( $container );
1874 if ( strlen( $object ??
'' ) ) {
1875 $parts[] = str_replace(
"%2F",
"/", rawurlencode( $object ) );
1878 return implode(
'/', $parts );
1886 return [
'x-auth-token' => $creds[
'auth_token'] ];
1895 private function getCredsCacheKey( $username ) {
1896 return 'swiftcredentials:' . md5( $username .
':' . $this->swiftAuthUrl );
1913 private function requestWithAuth( array $req, array $options = [] ) {
1914 return $this->requestMultiWithAuth( [ $req ], $options )[0][
'response'];
1926 private function requestMultiWithAuth( array $reqs, $options = [] ) {
1927 $remainingTries = 2;
1931 foreach ( $reqs as &$req ) {
1932 if ( !isset( $req[
'response'] ) ) {
1933 $req[
'response'] = $this->getAuthFailureResponse();
1938 foreach ( $reqs as &$req ) {
1939 '@phan-var array $req';
1940 if ( isset( $req[
'response'] ) ) {
1943 if ( $req[
'response'][
'code'] !== 401 ) {
1947 $req[
'headers'] = $this->
authTokenHeaders( $auth ) + ( $req[
'headers'] ?? [] );
1948 $req[
'url'] = $this->
storageUrl( $auth, $req[
'container'], $req[
'relPath'] ??
null );
1951 $reqs = $this->http->runMulti( $reqs, $options + self::DEFAULT_HTTP_OPTIONS );
1952 if ( --$remainingTries > 0 ) {
1954 foreach ( $reqs as $req ) {
1955 if ( $req[
'response'][
'code'] === 401 ) {
1956 $auth = $this->refreshAuthentication();
1974 private function getAuthFailureResponse() {
1984 'error' => self::AUTH_FAILURE_ERROR,
1985 4 => self::AUTH_FAILURE_ERROR
1996 private function isAuthFailureResponse( $code, $error ) {
1997 return $code === 0 && $error === self::AUTH_FAILURE_ERROR;
2012 public function onError( $status, $func, array
$params, $err =
'', $code = 0, $desc =
'', $body =
'' ) {
2013 if ( $this->isAuthFailureResponse( $code, $err ) ) {
2015 $status->fatal(
'backend-fail-connect', $this->name );
2021 $status->fatal(
'backend-fail-internal', $this->name );
2023 $msg =
"HTTP {code} ({desc}) in '{func}' (given '{req_params}')";
2028 'req_params' => FormatJson::encode(
$params ),
2032 $msgParams[
'err'] = $err;
2034 if ( $code == 502 ) {
2035 $msg .=
' ({truncatedBody})';
2036 $msgParams[
'truncatedBody'] = substr( strip_tags( $body ), 0, 100 );
2038 $this->logger->error( $msg, $msgParams );
2043class_alias( SwiftFileBackend::class,
'SwiftFileBackend' );
array $params
The job parameters.
Resource locking handling.
Store key-value entries in a size-limited in-memory LRU cache.
Generic operation result class Has warning/error list, boolean status and arbitrary value.