1<?php 2/** 3 * Proxy backend that mirrors writes to several internal backends. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup FileBackend 22 */ 23 24use Wikimedia\Timestamp\ConvertibleTimestamp; 25 26/** 27 * @brief Proxy backend that mirrors writes to several internal backends. 28 * 29 * This class defines a multi-write backend. Multiple backends can be 30 * registered to this proxy backend and it will act as a single backend. 31 * Use this when all access to those backends is through this proxy backend. 32 * At least one of the backends must be declared the "master" backend. 33 * 34 * Only use this class when transitioning from one storage system to another. 35 * 36 * Read operations are only done on the 'master' backend for consistency. 37 * Write operations are performed on all backends, starting with the master. 38 * This makes a best-effort to have transactional semantics, but since requests 39 * may sometimes fail, the use of "autoResync" or background scripts to fix 40 * inconsistencies is important. 41 * 42 * @ingroup FileBackend 43 * @since 1.19 44 */ 45class FileBackendMultiWrite extends FileBackend { 46 /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */ 47 protected $backends = []; 48 49 /** @var int Index of master backend */ 50 protected $masterIndex = -1; 51 /** @var int Index of read affinity backend */ 52 protected $readIndex = -1; 53 54 /** @var int Bitfield */ 55 protected $syncChecks = 0; 56 /** @var string|bool */ 57 protected $autoResync = false; 58 59 /** @var bool */ 60 protected $asyncWrites = false; 61 62 /** @var int Compare file sizes among backends */ 63 private const CHECK_SIZE = 1; 64 /** @var int Compare file mtimes among backends */ 65 private const CHECK_TIME = 2; 66 /** @var int Compare file hashes among backends */ 67 private const CHECK_SHA1 = 4; 68 69 /** 70 * Construct a proxy backend that consists of several internal backends. 71 * Locking, journaling, and read-only checks are handled by the proxy backend. 72 * 73 * Additional $config params include: 74 * - backends : Array of backend config and multi-backend settings. 75 * Each value is the config used in the constructor of a 76 * FileBackendStore class, but with these additional settings: 77 * - class : The name of the backend class 78 * - isMultiMaster : This must be set for one backend. 79 * - readAffinity : Use this for reads without 'latest' set. 80 * - syncChecks : Integer bitfield of internal backend sync checks to perform. 81 * Possible bits include the FileBackendMultiWrite::CHECK_* constants. 82 * There are constants for SIZE, TIME, and SHA1. 83 * The checks are done before allowing any file operations. 84 * - autoResync : Automatically resync the clone backends to the master backend 85 * when pre-operation sync checks fail. This should only be used 86 * if the master backend is stable and not missing any files. 87 * Use "conservative" to limit resyncing to copying newer master 88 * backend files over older (or non-existing) clone backend files. 89 * Cases that cannot be handled will result in operation abortion. 90 * - replication : Set to 'async' to defer file operations on the non-master backends. 91 * This will apply such updates post-send for web requests. Note that 92 * any checks from "syncChecks" are still synchronous. 93 * 94 * @param array $config 95 * @throws LogicException 96 */ 97 public function __construct( array $config ) { 98 parent::__construct( $config ); 99 $this->syncChecks = $config['syncChecks'] ?? self::CHECK_SIZE; 100 $this->autoResync = $config['autoResync'] ?? false; 101 $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async'; 102 // Construct backends here rather than via registration 103 // to keep these backends hidden from outside the proxy. 104 $namesUsed = []; 105 foreach ( $config['backends'] as $index => $beConfig ) { 106 $name = $beConfig['name']; 107 if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates 108 throw new LogicException( "Two or more backends defined with the name $name." ); 109 } 110 $namesUsed[$name] = 1; 111 // Alter certain sub-backend settings for sanity 112 unset( $beConfig['readOnly'] ); // use proxy backend setting 113 unset( $beConfig['fileJournal'] ); // use proxy backend journal 114 unset( $beConfig['lockManager'] ); // lock under proxy backend 115 $beConfig['domainId'] = $this->domainId; // use the proxy backend wiki ID 116 $beConfig['logger'] = $this->logger; // use the proxy backend logger 117 if ( !empty( $beConfig['isMultiMaster'] ) ) { 118 if ( $this->masterIndex >= 0 ) { 119 throw new LogicException( 'More than one master backend defined.' ); 120 } 121 $this->masterIndex = $index; // this is the "master" 122 $beConfig['fileJournal'] = $this->fileJournal; // log under proxy backend 123 } 124 if ( !empty( $beConfig['readAffinity'] ) ) { 125 $this->readIndex = $index; // prefer this for reads 126 } 127 // Create sub-backend object 128 if ( !isset( $beConfig['class'] ) ) { 129 throw new InvalidArgumentException( 'No class given for a backend config.' ); 130 } 131 $class = $beConfig['class']; 132 $this->backends[$index] = new $class( $beConfig ); 133 } 134 if ( $this->masterIndex < 0 ) { // need backends and must have a master 135 throw new LogicException( 'No master backend defined.' ); 136 } 137 if ( $this->readIndex < 0 ) { 138 $this->readIndex = $this->masterIndex; // default 139 } 140 } 141 142 final protected function doOperationsInternal( array $ops, array $opts ) { 143 $status = $this->newStatus(); 144 145 $fname = __METHOD__; 146 $mbe = $this->backends[$this->masterIndex]; // convenience 147 148 // Acquire any locks as needed 149 $scopeLock = null; 150 if ( empty( $opts['nonLocking'] ) ) { 151 $scopeLock = $this->getScopedLocksForOps( $ops, $status ); 152 if ( !$status->isOK() ) { 153 return $status; // abort 154 } 155 } 156 // Get the list of paths to read/write 157 $relevantPaths = $this->fileStoragePathsForOps( $ops ); 158 // Clear any cache entries (after locks acquired) 159 $this->clearCache( $relevantPaths ); 160 $opts['preserveCache'] = true; // only locked files are cached 161 // Check if the paths are valid and accessible on all backends 162 $status->merge( $this->accessibilityCheck( $relevantPaths ) ); 163 if ( !$status->isOK() ) { 164 return $status; // abort 165 } 166 // Do a consistency check to see if the backends are consistent 167 $syncStatus = $this->consistencyCheck( $relevantPaths ); 168 if ( !$syncStatus->isOK() ) { 169 $this->logger->error( 170 "$fname: failed sync check: " . FormatJson::encode( $relevantPaths ) 171 ); 172 // Try to resync the clone backends to the master on the spot 173 if ( 174 $this->autoResync === false || 175 !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() 176 ) { 177 $status->merge( $syncStatus ); 178 179 return $status; // abort 180 } 181 } 182 // Actually attempt the operation batch on the master backend 183 $realOps = $this->substOpBatchPaths( $ops, $mbe ); 184 $masterStatus = $mbe->doOperations( $realOps, $opts ); 185 $status->merge( $masterStatus ); 186 // Propagate the operations to the clone backends if there were no unexpected errors 187 // and everything didn't fail due to predicted errors. If $ops only had one operation, 188 // this might avoid backend sync inconsistencies. 189 if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) { 190 foreach ( $this->backends as $index => $backend ) { 191 if ( $index === $this->masterIndex ) { 192 continue; // done already 193 } 194 195 $realOps = $this->substOpBatchPaths( $ops, $backend ); 196 if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) { 197 // Bind $scopeLock to the callback to preserve locks 198 DeferredUpdates::addCallableUpdate( 199 function () use ( 200 $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname 201 ) { 202 $this->logger->debug( 203 "$fname: '{$backend->getName()}' async replication; paths: " . 204 FormatJson::encode( $relevantPaths ) 205 ); 206 $backend->doOperations( $realOps, $opts ); 207 } 208 ); 209 } else { 210 $this->logger->debug( 211 "$fname: '{$backend->getName()}' sync replication; paths: " . 212 FormatJson::encode( $relevantPaths ) 213 ); 214 $status->merge( $backend->doOperations( $realOps, $opts ) ); 215 } 216 } 217 } 218 // Make 'success', 'successCount', and 'failCount' fields reflect 219 // the overall operation, rather than all the batches for each backend. 220 // Do this by only using success values from the master backend's batch. 221 $status->success = $masterStatus->success; 222 $status->successCount = $masterStatus->successCount; 223 $status->failCount = $masterStatus->failCount; 224 225 return $status; 226 } 227 228 /** 229 * Check that a set of files are consistent across all internal backends 230 * 231 * This method should only be called if the files are locked or the backend 232 * is in read-only mode 233 * 234 * @param array $paths List of storage paths 235 * @return StatusValue 236 */ 237 public function consistencyCheck( array $paths ) { 238 $status = $this->newStatus(); 239 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) { 240 return $status; // skip checks 241 } 242 243 // Preload all of the stat info in as few round trips as possible 244 foreach ( $this->backends as $backend ) { 245 $realPaths = $this->substPaths( $paths, $backend ); 246 $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] ); 247 } 248 249 foreach ( $paths as $path ) { 250 $params = [ 'src' => $path, 'latest' => true ]; 251 // Get the state of the file on the master backend 252 $masterBackend = $this->backends[$this->masterIndex]; 253 $masterParams = $this->substOpPaths( $params, $masterBackend ); 254 $masterStat = $masterBackend->getFileStat( $masterParams ); 255 if ( $masterStat === self::STAT_ERROR ) { 256 $status->fatal( 'backend-fail-stat', $path ); 257 continue; 258 } 259 if ( $this->syncChecks & self::CHECK_SHA1 ) { 260 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); 261 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { 262 $status->fatal( 'backend-fail-hash', $path ); 263 continue; 264 } 265 } else { 266 $masterSha1 = null; // unused 267 } 268 269 // Check if all clone backends agree with the master... 270 foreach ( $this->backends as $index => $cloneBackend ) { 271 if ( $index === $this->masterIndex ) { 272 continue; // master 273 } 274 275 // Get the state of the file on the clone backend 276 $cloneParams = $this->substOpPaths( $params, $cloneBackend ); 277 $cloneStat = $cloneBackend->getFileStat( $cloneParams ); 278 279 if ( $masterStat ) { 280 // File exists in the master backend 281 if ( !$cloneStat ) { 282 // File is missing from the clone backend 283 $status->fatal( 'backend-fail-synced', $path ); 284 } elseif ( 285 ( $this->syncChecks & self::CHECK_SIZE ) && 286 $cloneStat['size'] !== $masterStat['size'] 287 ) { 288 // File in the clone backend is different 289 $status->fatal( 'backend-fail-synced', $path ); 290 } elseif ( 291 ( $this->syncChecks & self::CHECK_TIME ) && 292 abs( 293 ConvertibleTimestamp::convert( TS_UNIX, $masterStat['mtime'] ) - 294 ConvertibleTimestamp::convert( TS_UNIX, $cloneStat['mtime'] ) 295 ) > 30 296 ) { 297 // File in the clone backend is significantly newer or older 298 $status->fatal( 'backend-fail-synced', $path ); 299 } elseif ( 300 ( $this->syncChecks & self::CHECK_SHA1 ) && 301 $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1 302 ) { 303 // File in the clone backend is different 304 $status->fatal( 'backend-fail-synced', $path ); 305 } 306 } else { 307 // File does not exist in the master backend 308 if ( $cloneStat ) { 309 // Stray file exists in the clone backend 310 $status->fatal( 'backend-fail-synced', $path ); 311 } 312 } 313 } 314 } 315 316 return $status; 317 } 318 319 /** 320 * Check that a set of file paths are usable across all internal backends 321 * 322 * @param array $paths List of storage paths 323 * @return StatusValue 324 */ 325 public function accessibilityCheck( array $paths ) { 326 $status = $this->newStatus(); 327 if ( count( $this->backends ) <= 1 ) { 328 return $status; // skip checks 329 } 330 331 foreach ( $paths as $path ) { 332 foreach ( $this->backends as $backend ) { 333 $realPath = $this->substPaths( $path, $backend ); 334 if ( !$backend->isPathUsableInternal( $realPath ) ) { 335 $status->fatal( 'backend-fail-usable', $path ); 336 } 337 } 338 } 339 340 return $status; 341 } 342 343 /** 344 * Check that a set of files are consistent across all internal backends 345 * and re-synchronize those files against the "multi master" if needed. 346 * 347 * This method should only be called if the files are locked 348 * 349 * @param array $paths List of storage paths 350 * @param string|bool $resyncMode False, True, or "conservative"; see __construct() 351 * @return StatusValue 352 */ 353 public function resyncFiles( array $paths, $resyncMode = true ) { 354 $status = $this->newStatus(); 355 356 $fname = __METHOD__; 357 foreach ( $paths as $path ) { 358 $params = [ 'src' => $path, 'latest' => true ]; 359 // Get the state of the file on the master backend 360 $masterBackend = $this->backends[$this->masterIndex]; 361 $masterParams = $this->substOpPaths( $params, $masterBackend ); 362 $masterPath = $masterParams['src']; 363 $masterStat = $masterBackend->getFileStat( $masterParams ); 364 if ( $masterStat === self::STAT_ERROR ) { 365 $status->fatal( 'backend-fail-stat', $path ); 366 $this->logger->error( "$fname: file '$masterPath' is not available" ); 367 continue; 368 } 369 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); 370 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { 371 $status->fatal( 'backend-fail-hash', $path ); 372 $this->logger->error( "$fname: file '$masterPath' hash does not match stat" ); 373 continue; 374 } 375 376 // Check of all clone backends agree with the master... 377 foreach ( $this->backends as $index => $cloneBackend ) { 378 if ( $index === $this->masterIndex ) { 379 continue; // master 380 } 381 382 // Get the state of the file on the clone backend 383 $cloneParams = $this->substOpPaths( $params, $cloneBackend ); 384 $clonePath = $cloneParams['src']; 385 $cloneStat = $cloneBackend->getFileStat( $cloneParams ); 386 if ( $cloneStat === self::STAT_ERROR ) { 387 $status->fatal( 'backend-fail-stat', $path ); 388 $this->logger->error( "$fname: file '$clonePath' is not available" ); 389 continue; 390 } 391 $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams ); 392 if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) { 393 $status->fatal( 'backend-fail-hash', $path ); 394 $this->logger->error( "$fname: file '$clonePath' hash does not match stat" ); 395 continue; 396 } 397 398 if ( $masterSha1 === $cloneSha1 ) { 399 // File is either the same in both backends or absent from both backends 400 $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" ); 401 } elseif ( $masterSha1 !== false ) { 402 // File is either missing from or different in the clone backend 403 if ( 404 $resyncMode === 'conservative' && 405 $cloneStat && 406 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable 407 $cloneStat['mtime'] > $masterStat['mtime'] 408 ) { 409 // Do not replace files with older ones; reduces the risk of data loss 410 $status->fatal( 'backend-fail-synced', $path ); 411 } else { 412 // Copy the master backend file to the clone backend in overwrite mode 413 $fsFile = $masterBackend->getLocalReference( $masterParams ); 414 $status->merge( $cloneBackend->quickStore( [ 415 'src' => $fsFile, 416 'dst' => $clonePath 417 ] ) ); 418 } 419 } elseif ( $masterStat === false ) { 420 // Stray file exists in the clone backend 421 if ( $resyncMode === 'conservative' ) { 422 // Do not delete stray files; reduces the risk of data loss 423 $status->fatal( 'backend-fail-synced', $path ); 424 $this->logger->error( "$fname: not allowed to delete file '$clonePath'" ); 425 } else { 426 // Delete the stay file from the clone backend 427 $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) ); 428 } 429 } 430 } 431 } 432 433 if ( !$status->isOK() ) { 434 $this->logger->error( "$fname: failed to resync: " . FormatJson::encode( $paths ) ); 435 } 436 437 return $status; 438 } 439 440 /** 441 * Get a list of file storage paths to read or write for a list of operations 442 * 443 * @param array $ops Same format as doOperations() 444 * @return array List of storage paths to files (does not include directories) 445 */ 446 protected function fileStoragePathsForOps( array $ops ) { 447 $paths = []; 448 foreach ( $ops as $op ) { 449 if ( isset( $op['src'] ) ) { 450 // For things like copy/move/delete with "ignoreMissingSource" and there 451 // is no source file, nothing should happen and there should be no errors. 452 if ( empty( $op['ignoreMissingSource'] ) 453 || $this->fileExists( [ 'src' => $op['src'] ] ) 454 ) { 455 $paths[] = $op['src']; 456 } 457 } 458 if ( isset( $op['srcs'] ) ) { 459 $paths = array_merge( $paths, $op['srcs'] ); 460 } 461 if ( isset( $op['dst'] ) ) { 462 $paths[] = $op['dst']; 463 } 464 } 465 466 return array_values( array_unique( array_filter( $paths, [ FileBackend::class, 'isStoragePath' ] ) ) ); 467 } 468 469 /** 470 * Substitute the backend name in storage path parameters 471 * for a set of operations with that of a given internal backend. 472 * 473 * @param array $ops List of file operation arrays 474 * @param FileBackendStore $backend 475 * @return array 476 */ 477 protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) { 478 $newOps = []; // operations 479 foreach ( $ops as $op ) { 480 $newOp = $op; // operation 481 foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) { 482 if ( isset( $newOp[$par] ) ) { // string or array 483 $newOp[$par] = $this->substPaths( $newOp[$par], $backend ); 484 } 485 } 486 $newOps[] = $newOp; 487 } 488 489 return $newOps; 490 } 491 492 /** 493 * Same as substOpBatchPaths() but for a single operation 494 * 495 * @param array $ops File operation array 496 * @param FileBackendStore $backend 497 * @return array 498 */ 499 protected function substOpPaths( array $ops, FileBackendStore $backend ) { 500 $newOps = $this->substOpBatchPaths( [ $ops ], $backend ); 501 502 return $newOps[0]; 503 } 504 505 /** 506 * Substitute the backend of storage paths with an internal backend's name 507 * 508 * @param array|string $paths List of paths or single string path 509 * @param FileBackendStore $backend 510 * @return string[]|string 511 */ 512 protected function substPaths( $paths, FileBackendStore $backend ) { 513 return preg_replace( 514 '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!', 515 StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ), 516 $paths // string or array 517 ); 518 } 519 520 /** 521 * Substitute the backend of internal storage paths with the proxy backend's name 522 * 523 * @param array|string $paths List of paths or single string path 524 * @param FileBackendStore $backend internal storage backend 525 * @return string[]|string 526 */ 527 protected function unsubstPaths( $paths, FileBackendStore $backend ) { 528 return preg_replace( 529 '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!', 530 StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ), 531 $paths // string or array 532 ); 533 } 534 535 /** 536 * @param array[] $ops File operations for FileBackend::doOperations() 537 * @return bool Whether there are file path sources with outside lifetime/ownership 538 */ 539 protected function hasVolatileSources( array $ops ) { 540 foreach ( $ops as $op ) { 541 if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) { 542 return true; // source file might be deleted anytime after do*Operations() 543 } 544 } 545 546 return false; 547 } 548 549 protected function doQuickOperationsInternal( array $ops, array $opts ) { 550 $status = $this->newStatus(); 551 // Do the operations on the master backend; setting StatusValue fields 552 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); 553 $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps ); 554 $status->merge( $masterStatus ); 555 // Propagate the operations to the clone backends... 556 foreach ( $this->backends as $index => $backend ) { 557 if ( $index === $this->masterIndex ) { 558 continue; // done already 559 } 560 561 $realOps = $this->substOpBatchPaths( $ops, $backend ); 562 if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) { 563 DeferredUpdates::addCallableUpdate( 564 static function () use ( $backend, $realOps ) { 565 $backend->doQuickOperations( $realOps ); 566 } 567 ); 568 } else { 569 $status->merge( $backend->doQuickOperations( $realOps ) ); 570 } 571 } 572 // Make 'success', 'successCount', and 'failCount' fields reflect 573 // the overall operation, rather than all the batches for each backend. 574 // Do this by only using success values from the master backend's batch. 575 $status->success = $masterStatus->success; 576 $status->successCount = $masterStatus->successCount; 577 $status->failCount = $masterStatus->failCount; 578 579 return $status; 580 } 581 582 protected function doPrepare( array $params ) { 583 return $this->doDirectoryOp( 'prepare', $params ); 584 } 585 586 protected function doSecure( array $params ) { 587 return $this->doDirectoryOp( 'secure', $params ); 588 } 589 590 protected function doPublish( array $params ) { 591 return $this->doDirectoryOp( 'publish', $params ); 592 } 593 594 protected function doClean( array $params ) { 595 return $this->doDirectoryOp( 'clean', $params ); 596 } 597 598 /** 599 * @param string $method One of (doPrepare,doSecure,doPublish,doClean) 600 * @param array $params Method arguments 601 * @return StatusValue 602 */ 603 protected function doDirectoryOp( $method, array $params ) { 604 $status = $this->newStatus(); 605 606 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); 607 $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams ); 608 $status->merge( $masterStatus ); 609 610 foreach ( $this->backends as $index => $backend ) { 611 if ( $index === $this->masterIndex ) { 612 continue; // already done 613 } 614 615 $realParams = $this->substOpPaths( $params, $backend ); 616 if ( $this->asyncWrites ) { 617 DeferredUpdates::addCallableUpdate( 618 static function () use ( $backend, $method, $realParams ) { 619 $backend->$method( $realParams ); 620 } 621 ); 622 } else { 623 $status->merge( $backend->$method( $realParams ) ); 624 } 625 } 626 627 return $status; 628 } 629 630 public function concatenate( array $params ) { 631 $status = $this->newStatus(); 632 // We are writing to an FS file, so we don't need to do this per-backend 633 $index = $this->getReadIndexFromParams( $params ); 634 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 635 636 $status->merge( $this->backends[$index]->concatenate( $realParams ) ); 637 638 return $status; 639 } 640 641 public function fileExists( array $params ) { 642 $index = $this->getReadIndexFromParams( $params ); 643 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 644 645 return $this->backends[$index]->fileExists( $realParams ); 646 } 647 648 public function getFileTimestamp( array $params ) { 649 $index = $this->getReadIndexFromParams( $params ); 650 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 651 652 return $this->backends[$index]->getFileTimestamp( $realParams ); 653 } 654 655 public function getFileSize( array $params ) { 656 $index = $this->getReadIndexFromParams( $params ); 657 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 658 659 return $this->backends[$index]->getFileSize( $realParams ); 660 } 661 662 public function getFileStat( array $params ) { 663 $index = $this->getReadIndexFromParams( $params ); 664 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 665 666 return $this->backends[$index]->getFileStat( $realParams ); 667 } 668 669 public function getFileXAttributes( array $params ) { 670 $index = $this->getReadIndexFromParams( $params ); 671 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 672 673 return $this->backends[$index]->getFileXAttributes( $realParams ); 674 } 675 676 public function getFileContentsMulti( array $params ) { 677 $index = $this->getReadIndexFromParams( $params ); 678 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 679 680 $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams ); 681 682 $contents = []; // (path => FSFile) mapping using the proxy backend's name 683 foreach ( $contentsM as $path => $data ) { 684 $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data; 685 } 686 687 return $contents; 688 } 689 690 public function getFileSha1Base36( array $params ) { 691 $index = $this->getReadIndexFromParams( $params ); 692 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 693 694 return $this->backends[$index]->getFileSha1Base36( $realParams ); 695 } 696 697 public function getFileProps( array $params ) { 698 $index = $this->getReadIndexFromParams( $params ); 699 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 700 701 return $this->backends[$index]->getFileProps( $realParams ); 702 } 703 704 public function streamFile( array $params ) { 705 $index = $this->getReadIndexFromParams( $params ); 706 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 707 708 return $this->backends[$index]->streamFile( $realParams ); 709 } 710 711 public function getLocalReferenceMulti( array $params ) { 712 $index = $this->getReadIndexFromParams( $params ); 713 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 714 715 $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams ); 716 717 $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name 718 foreach ( $fsFilesM as $path => $fsFile ) { 719 $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile; 720 } 721 722 return $fsFiles; 723 } 724 725 public function getLocalCopyMulti( array $params ) { 726 $index = $this->getReadIndexFromParams( $params ); 727 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 728 729 $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams ); 730 731 $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name 732 foreach ( $tempFilesM as $path => $tempFile ) { 733 $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile; 734 } 735 736 return $tempFiles; 737 } 738 739 public function getFileHttpUrl( array $params ) { 740 $index = $this->getReadIndexFromParams( $params ); 741 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 742 743 return $this->backends[$index]->getFileHttpUrl( $realParams ); 744 } 745 746 public function directoryExists( array $params ) { 747 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); 748 749 return $this->backends[$this->masterIndex]->directoryExists( $realParams ); 750 } 751 752 public function getDirectoryList( array $params ) { 753 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); 754 755 return $this->backends[$this->masterIndex]->getDirectoryList( $realParams ); 756 } 757 758 public function getFileList( array $params ) { 759 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); 760 761 return $this->backends[$this->masterIndex]->getFileList( $realParams ); 762 } 763 764 public function getFeatures() { 765 return $this->backends[$this->masterIndex]->getFeatures(); 766 } 767 768 public function clearCache( array $paths = null ) { 769 foreach ( $this->backends as $backend ) { 770 $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null; 771 $backend->clearCache( $realPaths ); 772 } 773 } 774 775 public function preloadCache( array $paths ) { 776 $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] ); 777 $this->backends[$this->readIndex]->preloadCache( $realPaths ); 778 } 779 780 public function preloadFileStat( array $params ) { 781 $index = $this->getReadIndexFromParams( $params ); 782 $realParams = $this->substOpPaths( $params, $this->backends[$index] ); 783 784 return $this->backends[$index]->preloadFileStat( $realParams ); 785 } 786 787 public function getScopedLocksForOps( array $ops, StatusValue $status ) { 788 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); 789 $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps ); 790 // Get the paths to lock from the master backend 791 $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps ); 792 // Get the paths under the proxy backend's name 793 $pbPaths = [ 794 LockManager::LOCK_UW => $this->unsubstPaths( 795 $paths[LockManager::LOCK_UW], 796 $this->backends[$this->masterIndex] 797 ), 798 LockManager::LOCK_EX => $this->unsubstPaths( 799 $paths[LockManager::LOCK_EX], 800 $this->backends[$this->masterIndex] 801 ) 802 ]; 803 804 // Actually acquire the locks 805 return $this->getScopedFileLocks( $pbPaths, 'mixed', $status ); 806 } 807 808 /** 809 * @param array $params 810 * @return int The master or read affinity backend index, based on $params['latest'] 811 */ 812 protected function getReadIndexFromParams( array $params ) { 813 return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex; 814 } 815} 816