24 use Wikimedia\AtEase\AtEase;
25 use Wikimedia\Timestamp\ConvertibleTimestamp;
84 parent::__construct( $config );
85 $this->mimeCallback = $config[
'mimeCallback'] ??
null;
88 $this->cheapCache =
new MapCacheLRU( self::CACHE_CHEAP_SIZE );
89 $this->expensiveCache =
new MapCacheLRU( self::CACHE_EXPENSIVE_SIZE );
143 if ( !isset( $params[
'dstExists'] ) || $params[
'dstExists'] ) {
186 if ( !isset( $params[
'dstExists'] ) || $params[
'dstExists'] ) {
226 if ( !isset( $params[
'dstExists'] ) || $params[
'dstExists'] ) {
295 $this->
clearCache( [ $params[
'src'], $params[
'dst'] ] );
297 if ( !isset( $params[
'dstExists'] ) || $params[
'dstExists'] ) {
310 unset( $params[
'async'] );
315 if ( $nsrc !== $ndst &&
$status->isOK() ) {
342 if ( count( $params[
'headers'] ) ) {
383 $start_time = microtime(
true );
385 $sec = microtime(
true ) - $start_time;
387 $this->logger->error( static::class .
"-{$this->name}" .
388 " failed to concatenate " . count( $params[
'srcs'] ) .
" file(s) [$sec sec]" );
402 $tmpPath = $params[
'dst'];
403 unset( $params[
'latest'] );
406 AtEase::suppressWarnings();
407 $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
408 AtEase::restoreWarnings();
410 $status->fatal(
'backend-fail-opentemp', $tmpPath );
417 foreach ( $fsFiles as
$path => &$fsFile ) {
430 $tmpHandle = fopen( $tmpPath,
'ab' );
431 if ( $tmpHandle ===
false ) {
432 $status->fatal(
'backend-fail-opentemp', $tmpPath );
438 foreach ( $fsFiles as $virtualSource => $fsFile ) {
440 $sourceHandle = fopen( $fsFile->getPath(),
'rb' );
441 if ( $sourceHandle ===
false ) {
442 fclose( $tmpHandle );
443 $status->fatal(
'backend-fail-read', $virtualSource );
448 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
449 fclose( $sourceHandle );
450 fclose( $tmpHandle );
451 $status->fatal(
'backend-fail-writetemp', $tmpPath );
455 fclose( $sourceHandle );
457 if ( !fclose( $tmpHandle ) ) {
458 $status->fatal(
'backend-fail-closetemp', $tmpPath );
474 if ( $dir ===
null ) {
475 $status->fatal(
'backend-fail-invalidpath', $params[
'dir'] );
480 if ( $shard !==
null ) {
483 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
504 final protected function doSecure( array $params ) {
510 if ( $dir ===
null ) {
511 $status->fatal(
'backend-fail-invalidpath', $params[
'dir'] );
516 if ( $shard !==
null ) {
519 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
546 if ( $dir ===
null ) {
547 $status->fatal(
'backend-fail-invalidpath', $params[
'dir'] );
552 if ( $shard !==
null ) {
555 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
576 final protected function doClean( array $params ) {
584 if ( $subDirsRel !==
null ) {
585 foreach ( $subDirsRel as $subDirRel ) {
586 $subDir = $params[
'dir'] .
"/{$subDirRel}";
587 $status->merge( $this->
doClean( [
'dir' => $subDir ] + $params ) );
589 unset( $subDirsRel );
594 if ( $dir ===
null ) {
595 $status->fatal(
'backend-fail-invalidpath', $params[
'dir'] );
601 $filesLockEx = [ $params[
'dir'] ];
608 if ( $shard !==
null ) {
612 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
639 if ( is_array( $stat ) ) {
643 return ( $stat === self::$RES_ABSENT ) ? false : self::EXISTENCE_ERROR;
651 if ( is_array( $stat ) ) {
652 return $stat[
'mtime'];
655 return self::TIMESTAMP_FAIL;
663 if ( is_array( $stat ) ) {
664 return $stat[
'size'];
667 return self::SIZE_FAIL;
675 if (
$path ===
null ) {
676 return self::STAT_ERROR;
681 $latest = !empty( $params[
'latest'] );
683 $requireSHA1 = !empty( $params[
'requireSHA1'] );
685 $stat = $this->cheapCache->getField(
$path,
'stat', self::CACHE_TTL );
694 ( $requireSHA1 && is_array( $stat ) && !isset( $stat[
'sha1'] ) )
698 $stat = $this->cheapCache->getField(
$path,
'stat', self::CACHE_TTL );
702 if ( is_array( $stat ) ) {
704 ( !$latest || $stat[
'latest'] ) &&
705 ( !$requireSHA1 || isset( $stat[
'sha1'] ) )
709 } elseif ( $stat === self::$ABSENT_LATEST ) {
710 return self::STAT_ABSENT;
711 } elseif ( $stat === self::$ABSENT_NORMAL ) {
713 return self::STAT_ABSENT;
721 if ( is_array( $stat ) ) {
725 return ( $stat === self::$RES_ERROR ) ? self::STAT_ERROR : self::STAT_ABSENT;
738 foreach ( $stats as
$path => $stat ) {
739 if ( is_array( $stat ) ) {
741 $stat[
'latest'] = $stat[
'latest'] ?? $latest;
743 $this->cheapCache->setField(
$path,
'stat', $stat );
744 if ( isset( $stat[
'sha1'] ) ) {
746 $this->cheapCache->setField(
749 [
'hash' => $stat[
'sha1'],
'latest' => $latest ]
752 if ( isset( $stat[
'xattr'] ) ) {
755 $this->cheapCache->setField(
758 [
'map' => $stat[
'xattr'],
'latest' => $latest ]
763 } elseif ( $stat === self::$RES_ABSENT ) {
764 $this->cheapCache->setField(
767 $latest ? self::$ABSENT_LATEST : self::$ABSENT_NORMAL
769 $this->cheapCache->setField(
772 [
'map' => self::XATTRS_FAIL,
'latest' => $latest ]
774 $this->cheapCache->setField(
777 [
'hash' => self::SHA1_FAIL,
'latest' => $latest ]
779 $this->logger->debug(
780 __METHOD__ .
': File {path} does not exist',
785 $this->logger->error(
786 __METHOD__ .
': Could not stat file {path}',
809 $contents[
$path] = self::CONTENT_FAIL;
824 if ( $fsFile instanceof
FSFile ) {
825 AtEase::suppressWarnings();
826 $content = file_get_contents( $fsFile->getPath() );
827 AtEase::restoreWarnings();
829 } elseif ( $fsFile === self::$RES_ABSENT ) {
844 if (
$path ===
null ) {
845 return self::XATTRS_FAIL;
847 $latest = !empty( $params[
'latest'] );
848 if ( $this->cheapCache->hasField(
$path,
'xattr', self::CACHE_TTL ) ) {
849 $stat = $this->cheapCache->getField(
$path,
'xattr' );
852 if ( !$latest || $stat[
'latest'] ) {
857 if ( is_array( $fields ) ) {
859 $this->cheapCache->setField(
862 [
'map' => $fields,
'latest' => $latest ]
864 } elseif ( $fields === self::$RES_ABSENT ) {
865 $this->cheapCache->setField(
868 [
'map' => self::XATTRS_FAIL,
'latest' => $latest ]
871 $fields = self::XATTRS_FAIL;
883 return [
'headers' => [],
'metadata' => [] ];
891 if (
$path ===
null ) {
892 return self::SHA1_FAIL;
894 $latest = !empty( $params[
'latest'] );
895 if ( $this->cheapCache->hasField(
$path,
'sha1', self::CACHE_TTL ) ) {
896 $stat = $this->cheapCache->getField(
$path,
'sha1' );
899 if ( !$latest || $stat[
'latest'] ) {
900 return $stat[
'hash'];
904 if ( is_string( $sha1 ) ) {
905 $this->cheapCache->setField(
908 [
'hash' => $sha1,
'latest' => $latest ]
910 } elseif ( $sha1 === self::$RES_ABSENT ) {
911 $this->cheapCache->setField(
914 [
'hash' => self::SHA1_FAIL,
'latest' => $latest ]
917 $sha1 = self::SHA1_FAIL;
930 if ( $fsFile instanceof
FSFile ) {
931 $sha1 = $fsFile->getSha1Base36();
955 $latest = !empty( $params[
'latest'] );
957 foreach ( $params[
'srcs'] as $src ) {
959 if (
$path ===
null ) {
960 $fsFiles[$src] =
null;
961 } elseif ( $this->expensiveCache->hasField(
$path,
'localRef' ) ) {
962 $val = $this->expensiveCache->getField(
$path,
'localRef' );
965 if ( !$latest || $val[
'latest'] ) {
966 $fsFiles[$src] = $val[
'object'];
971 $params[
'srcs'] = array_diff( $params[
'srcs'], array_keys( $fsFiles ) );
973 if ( $fsFile instanceof
FSFile ) {
974 $fsFiles[
$path] = $fsFile;
975 $this->expensiveCache->setField(
978 [
'object' => $fsFile,
'latest' => $latest ]
981 $fsFiles[
$path] =
null;
1003 foreach ( $tmpFiles as
$path => $tmpFile ) {
1005 $tmpFiles[
$path] =
null;
1025 return self::TEMPURL_ERROR;
1034 $params[
'options'] = $params[
'options'] ?? [];
1035 $params[
'headers'] = $params[
'headers'] ?? [];
1038 if ( ( empty( $params[
'headless'] ) || $params[
'headers'] ) && headers_sent() ) {
1039 print
"Headers already sent, terminating.\n";
1040 $status->fatal(
'backend-fail-stream', $params[
'src'] );
1070 $res = $streamer->stream( $params[
'headers'],
true, $params[
'options'], $flags );
1077 $status->fatal(
'backend-fail-stream', $params[
'src'] );
1085 if ( $dir ===
null ) {
1086 return self::EXISTENCE_ERROR;
1088 if ( $shard !==
null ) {
1091 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
1096 if ( $exists ===
true ) {
1099 } elseif ( $exists === self::$RES_ERROR ) {
1100 $res = self::EXISTENCE_ERROR;
1116 abstract protected function doDirectoryExists( $container, $dir, array $params );
1120 if ( $dir ===
null ) {
1121 return self::EXISTENCE_ERROR;
1123 if ( $shard !==
null ) {
1127 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
1150 if ( $dir ===
null ) {
1151 return self::LIST_ERROR;
1153 if ( $shard !==
null ) {
1157 $this->logger->debug( __METHOD__ .
": iterating over all container shards.\n" );
1191 'store' => StoreFileOp::class,
1192 'copy' => CopyFileOp::class,
1193 'move' => MoveFileOp::class,
1194 'delete' => DeleteFileOp::class,
1195 'create' => CreateFileOp::class,
1196 'describe' => DescribeFileOp::class,
1197 'null' => NullFileOp::class
1202 foreach ( $ops as $operation ) {
1203 $opName = $operation[
'op'];
1204 if ( isset( $supportedOps[$opName] ) ) {
1205 $class = $supportedOps[$opName];
1207 $params = $operation;
1209 $performOps[] =
new $class( $this, $params, $this->logger );
1230 $paths = [
'sh' => [],
'ex' => [] ];
1231 foreach ( $performOps as $fileOp ) {
1232 $paths[
'sh'] = array_merge( $paths[
'sh'], $fileOp->storagePathsRead() );
1233 $paths[
'ex'] = array_merge( $paths[
'ex'], $fileOp->storagePathsChanged() );
1236 $paths[
'sh'] = array_diff( $paths[
'sh'], $paths[
'ex'] );
1238 $paths[
'sh'] = array_merge( $paths[
'sh'], array_map(
'dirname', $paths[
'ex'] ) );
1258 $ops = array_map( [ $this,
'sanitizeOpHeaders' ], $ops );
1264 if ( empty( $opts[
'nonLocking'] ) ) {
1276 if ( empty( $opts[
'preserveCache'] ) ) {
1282 foreach ( $performOps as $performOp ) {
1283 $paths = array_merge( $paths, $performOp->storagePathsRead() );
1284 $paths = array_merge( $paths, $performOp->storagePathsChanged() );
1288 $this->cheapCache->setMaxSize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
1293 $ok = $this->
preloadFileStat( [
'srcs' => $paths,
'latest' =>
true ] );
1301 $subStatus = $this->
newStatus(
'backend-fail-internal', $this->name );
1302 foreach ( $ops as $i => $op ) {
1303 $subStatus->success[$i] =
false;
1304 ++$subStatus->failCount;
1306 $this->logger->error( static::class .
"-{$this->name} " .
1312 $status->success = $subStatus->success;
1315 $this->cheapCache->setMaxSize( self::CACHE_CHEAP_SIZE );
1326 $ops = array_map( [ $this,
'sanitizeOpHeaders' ], $ops );
1331 $supportedOps = [
'create',
'store',
'copy',
'move',
'delete',
'describe',
'null' ];
1333 $async = ( $this->parallelize ===
'implicit' && count( $ops ) > 1 );
1337 $fileOpHandles = [];
1338 $curFileOpHandles = [];
1340 foreach ( $ops as $index => $params ) {
1341 if ( !in_array( $params[
'op'], $supportedOps ) ) {
1342 throw new FileBackendError(
"Operation '{$params['op']}' is not supported." );
1344 $method = $params[
'op'] .
'Internal';
1345 $subStatus = $this->$method( [
'async' => $async ] + $params );
1347 if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
1348 $fileOpHandles[] = $curFileOpHandles;
1349 $curFileOpHandles = [];
1351 $curFileOpHandles[$index] = $subStatus->value;
1353 $statuses[$index] = $subStatus;
1356 if ( count( $curFileOpHandles ) ) {
1357 $fileOpHandles[] = $curFileOpHandles;
1360 foreach ( $fileOpHandles as $fileHandleBatch ) {
1364 foreach ( $statuses as $index => $subStatus ) {
1366 if ( $subStatus->isOK() ) {
1367 $status->success[$index] =
true;
1370 $status->success[$index] =
false;
1391 foreach ( $fileOpHandles as $fileOpHandle ) {
1393 throw new InvalidArgumentException(
"Expected FileBackendStoreOpHandle object." );
1394 } elseif ( $fileOpHandle->backend->getName() !== $this->
getName() ) {
1395 throw new InvalidArgumentException(
"Expected handle for this file backend." );
1400 foreach ( $fileOpHandles as $fileOpHandle ) {
1401 $fileOpHandle->closeResources();
1416 if ( count( $fileOpHandles ) ) {
1417 throw new FileBackendError(
"Backend does not support asynchronous operations." );
1435 static $longs = [
'content-disposition' ];
1437 if ( isset( $op[
'headers'] ) ) {
1439 foreach ( $op[
'headers'] as
$name => $value ) {
1441 $maxHVLen = in_array(
$name, $longs ) ? INF : 255;
1442 if ( strlen(
$name ) > 255 || strlen( $value ) > $maxHVLen ) {
1443 $this->logger->error(
"Header '{header}' is too long.", [
1444 'filebackend' => $this->name,
1445 'header' =>
"$name: $value",
1448 $newHeaders[
$name] = strlen( $value ) ? $value :
'';
1451 $op[
'headers'] = $newHeaders;
1459 foreach ( $paths as
$path ) {
1461 $fullConts[] = $fullCont;
1469 if ( is_array( $paths ) ) {
1470 $paths = array_map(
'FileBackend::normalizeStoragePath', $paths );
1471 $paths = array_filter( $paths,
'strlen' );
1473 if ( $paths ===
null ) {
1474 $this->cheapCache->clear();
1475 $this->expensiveCache->clear();
1477 foreach ( $paths as
$path ) {
1478 $this->cheapCache->clear(
$path );
1479 $this->expensiveCache->clear(
$path );
1499 $params[
'concurrency'] = ( $this->parallelize !==
'off' ) ? $this->concurrency : 1;
1501 if ( $stats ===
null ) {
1506 $latest = !empty( $params[
'latest'] );
1567 return (
bool)preg_match(
'/^[a-z0-9][a-z0-9-_.]{0,199}$/i', $container );
1585 if ( $backend === $this->name ) {
1587 if ( $relPath !==
null && self::isValidShortContainerName( $shortCont ) ) {
1592 if ( $relPath !==
null ) {
1595 if ( self::isValidContainerName( $container ) ) {
1598 if ( $container !==
null ) {
1599 return [ $container, $relPath, $cShard ];
1606 return [
null,
null, null ];
1626 if ( $cShard !==
null && substr( $relPath, -1 ) !==
'/' ) {
1627 return [ $container, $relPath ];
1630 return [
null, null ];
1643 if ( $levels == 1 || $levels == 2 ) {
1645 $char = (
$base == 36 ) ?
'[0-9a-z]' :
'[0-9a-f]';
1648 if ( $levels === 1 ) {
1649 $hashDirRegex =
'(' . $char .
')';
1652 $hashDirRegex = $char .
'/(' . $char .
'{2})';
1654 $hashDirRegex =
'(' . $char .
')/(' . $char .
')';
1661 if ( preg_match(
"!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1662 return '.' . implode(
'', array_slice( $m, 1 ) );
1682 return ( $shard !==
null );
1694 if ( isset( $this->shardViaHashLevels[$container] ) ) {
1695 $config = $this->shardViaHashLevels[$container];
1696 $hashLevels = (int)$config[
'levels'];
1697 if ( $hashLevels == 1 || $hashLevels == 2 ) {
1698 $hashBase = (int)$config[
'base'];
1699 if ( $hashBase == 16 || $hashBase == 36 ) {
1700 return [ $hashLevels, $hashBase, $config[
'repeat'] ];
1705 return [ 0, 0, false ];
1717 if ( $digits > 0 ) {
1718 $numShards =
$base ** $digits;
1719 for ( $index = 0; $index < $numShards; $index++ ) {
1720 $shards[] =
'.' . Wikimedia\base_convert( $index, 10,
$base, $digits );
1734 if ( $this->domainId !=
'' ) {
1735 return "{$this->domainId}-$container";
1764 return $relStoragePath;
1774 return "filebackend:{$this->name}:{$this->domainId}:container:{$container}";
1784 $this->memCache->set( $this->
containerCacheKey( $container ), $val, 14 * 86400 );
1794 if ( !$this->memCache->delete( $this->containerCacheKey( $container ), 300 ) ) {
1795 $this->logger->warning(
"Unable to delete stat cache for container {container}.",
1796 [
'filebackend' => $this->name,
'container' => $container ]
1815 foreach ( $items as $item ) {
1816 if ( self::isStoragePath( $item ) ) {
1818 } elseif ( is_string( $item ) ) {
1823 foreach ( $paths as
$path ) {
1825 if ( $fullCont !==
null ) {
1832 $values = $this->memCache->getMulti( array_keys( $contNames ) );
1833 foreach ( $values as $cacheKey => $val ) {
1834 $contInfo[$contNames[$cacheKey]] = $val;
1858 return "filebackend:{$this->name}:{$this->domainId}:file:" . sha1(
$path );
1871 if (
$path ===
null ) {
1874 $mtime = ConvertibleTimestamp::convert( TS_UNIX, $val[
'mtime'] );
1875 $ttl = $this->memCache->adaptiveTTL( $mtime, 7 * 86400, 300, 0.1 );
1878 $this->memCache->set( $key, $val, $ttl );
1891 if (
$path ===
null ) {
1894 if ( !$this->memCache->delete( $this->fileCacheKey(
$path ), 300 ) ) {
1895 $this->logger->warning(
"Unable to delete stat cache for file {path}.",
1896 [
'filebackend' => $this->name,
'path' =>
$path ]
1915 foreach ( $items as $item ) {
1916 if ( self::isStoragePath( $item ) ) {
1921 $paths = array_filter( $paths,
'strlen' );
1923 foreach ( $paths as
$path ) {
1925 if ( $rel !==
null ) {
1931 $values = $this->memCache->getMulti( array_keys( $pathNames ) );
1933 foreach ( array_filter( $values,
'is_array' ) as $cacheKey => $stat ) {
1934 $path = $pathNames[$cacheKey];
1937 unset( $stat[
'latest'] );
1939 $this->cheapCache->setField(
$path,
'stat', $stat );
1940 if ( isset( $stat[
'sha1'] ) && strlen( $stat[
'sha1'] ) == 31 ) {
1942 $this->cheapCache->setField(
1945 [
'hash' => $stat[
'sha1'],
'latest' =>
false ]
1948 if ( isset( $stat[
'xattr'] ) && is_array( $stat[
'xattr'] ) ) {
1951 $this->cheapCache->setField(
1954 [
'map' => $stat[
'xattr'],
'latest' =>
false ]
1968 $newXAttr = [
'headers' => [],
'metadata' => [] ];
1970 foreach ( $xattr[
'headers'] as
$name => $value ) {
1971 $newXAttr[
'headers'][strtolower(
$name )] = $value;
1974 foreach ( $xattr[
'metadata'] as
$name => $value ) {
1975 $newXAttr[
'metadata'][strtolower(
$name )] = $value;
1988 $opts[
'concurrency'] = 1;
1989 if ( $this->parallelize ===
'implicit' ) {
1990 if ( !isset( $opts[
'parallelize'] ) || $opts[
'parallelize'] ) {
1993 } elseif ( $this->parallelize ===
'explicit' ) {
1994 if ( !empty( $opts[
'parallelize'] ) ) {
2011 if ( $this->mimeCallback ) {
2012 return call_user_func_array( $this->mimeCallback, func_get_args() );
2015 $mime = ( $fsPath !== null ) ? mime_content_type( $fsPath ) :
false;
2016 return $mime ?:
'unknown/unknown';