3 * Proxy backend that mirrors writes to several internal backends.
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.
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.
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
21 * @ingroup FileBackend
24 namespace Wikimedia\FileBackend
;
26 use InvalidArgumentException
;
29 use Shellbox\Command\BoxedCommand
;
32 use Wikimedia\Timestamp\ConvertibleTimestamp
;
35 * @brief Proxy backend that mirrors writes to several internal backends.
37 * This class defines a multi-write backend. Multiple backends can be
38 * registered to this proxy backend and it will act as a single backend.
39 * Use this when all access to those backends is through this proxy backend.
40 * At least one of the backends must be declared the "master" backend.
42 * Only use this class when transitioning from one storage system to another.
44 * Read operations are only done on the 'master' backend for consistency.
45 * Except on getting list of thumbnails for write operations.
46 * Write operations are performed on all backends, starting with the master.
47 * This makes a best-effort to have transactional semantics, but since requests
48 * may sometimes fail, the use of "autoResync" or background scripts to fix
49 * inconsistencies is important.
51 * @ingroup FileBackend
54 class FileBackendMultiWrite
extends FileBackend
{
55 /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
56 protected $backends = [];
58 /** @var int Index of master backend */
59 protected $masterIndex = -1;
60 /** @var int Index of read affinity backend */
61 protected $readIndex = -1;
63 /** @var int Bitfield */
64 protected $syncChecks = 0;
65 /** @var string|bool */
66 protected $autoResync = false;
69 protected $asyncWrites = false;
71 /** @var int Compare file sizes among backends */
72 private const CHECK_SIZE
= 1;
73 /** @var int Compare file mtimes among backends */
74 private const CHECK_TIME
= 2;
75 /** @var int Compare file hashes among backends */
76 private const CHECK_SHA1
= 4;
79 * Construct a proxy backend that consists of several internal backends.
80 * Locking and read-only checks are handled by the proxy backend.
82 * Additional $config params include:
83 * - backends : Array of backend config and multi-backend settings.
84 * Each value is the config used in the constructor of a
85 * FileBackendStore class, but with these additional settings:
86 * - class : The name of the backend class
87 * - isMultiMaster : This must be set for one backend.
88 * - readAffinity : Use this for reads without 'latest' set.
89 * - syncChecks : Integer bitfield of internal backend sync checks to perform.
90 * Possible bits include the FileBackendMultiWrite::CHECK_* constants.
91 * There are constants for SIZE, TIME, and SHA1.
92 * The checks are done before allowing any file operations.
93 * - autoResync : Automatically resync the clone backends to the master backend
94 * when pre-operation sync checks fail. This should only be used
95 * if the master backend is stable and not missing any files.
96 * Use "conservative" to limit resyncing to copying newer master
97 * backend files over older (or non-existing) clone backend files.
98 * Cases that cannot be handled will result in operation abortion.
99 * - replication : Set to 'async' to defer file operations on the non-master backends.
100 * This will apply such updates post-send for web requests. Note that
101 * any checks from "syncChecks" are still synchronous.
103 * @param array $config
105 public function __construct( array $config ) {
106 parent
::__construct( $config );
107 $this->syncChecks
= $config['syncChecks'] ?? self
::CHECK_SIZE
;
108 $this->autoResync
= $config['autoResync'] ??
false;
109 $this->asyncWrites
= isset( $config['replication'] ) && $config['replication'] === 'async';
110 // Construct backends here rather than via registration
111 // to keep these backends hidden from outside the proxy.
113 foreach ( $config['backends'] as $index => $beConfig ) {
114 $name = $beConfig['name'];
115 if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
116 throw new LogicException( "Two or more backends defined with the name $name." );
118 $namesUsed[$name] = 1;
119 // Alter certain sub-backend settings
120 unset( $beConfig['readOnly'] ); // use proxy backend setting
121 unset( $beConfig['lockManager'] ); // lock under proxy backend
122 $beConfig['domainId'] = $this->domainId
; // use the proxy backend wiki ID
123 $beConfig['logger'] = $this->logger
; // use the proxy backend logger
124 if ( !empty( $beConfig['isMultiMaster'] ) ) {
125 if ( $this->masterIndex
>= 0 ) {
126 throw new LogicException( 'More than one master backend defined.' );
128 $this->masterIndex
= $index; // this is the "master"
130 if ( !empty( $beConfig['readAffinity'] ) ) {
131 $this->readIndex
= $index; // prefer this for reads
133 // Create sub-backend object
134 if ( !isset( $beConfig['class'] ) ) {
135 throw new InvalidArgumentException( 'No class given for a backend config.' );
137 $class = $beConfig['class'];
138 $this->backends
[$index] = new $class( $beConfig );
140 if ( $this->masterIndex
< 0 ) { // need backends and must have a master
141 throw new LogicException( 'No master backend defined.' );
143 if ( $this->readIndex
< 0 ) {
144 $this->readIndex
= $this->masterIndex
; // default
148 final protected function doOperationsInternal( array $ops, array $opts ) {
149 $status = $this->newStatus();
152 $mbe = $this->backends
[$this->masterIndex
]; // convenience
154 // Acquire any locks as needed
156 if ( empty( $opts['nonLocking'] ) ) {
157 $scopeLock = $this->getScopedLocksForOps( $ops, $status );
158 if ( !$status->isOK() ) {
159 return $status; // abort
162 // Get the list of paths to read/write
163 $relevantPaths = $this->fileStoragePathsForOps( $ops );
164 // Clear any cache entries (after locks acquired)
165 $this->clearCache( $relevantPaths );
166 $opts['preserveCache'] = true; // only locked files are cached
167 // Check if the paths are valid and accessible on all backends
168 $status->merge( $this->accessibilityCheck( $relevantPaths ) );
169 if ( !$status->isOK() ) {
170 return $status; // abort
172 // Do a consistency check to see if the backends are consistent
173 $syncStatus = $this->consistencyCheck( $relevantPaths );
174 if ( !$syncStatus->isOK() ) {
175 $this->logger
->error(
176 "$fname: failed sync check: " . implode( ', ', $relevantPaths )
178 // Try to resync the clone backends to the master on the spot
180 $this->autoResync
=== false ||
181 !$this->resyncFiles( $relevantPaths, $this->autoResync
)->isOK()
183 $status->merge( $syncStatus );
185 return $status; // abort
188 // Actually attempt the operation batch on the master backend
189 $realOps = $this->substOpBatchPaths( $ops, $mbe );
190 $masterStatus = $mbe->doOperations( $realOps, $opts );
191 $status->merge( $masterStatus );
192 // Propagate the operations to the clone backends if there were no unexpected errors
193 // and everything didn't fail due to predicted errors. If $ops only had one operation,
194 // this might avoid backend sync inconsistencies.
195 if ( $masterStatus->isOK() && $masterStatus->successCount
> 0 ) {
196 foreach ( $this->backends
as $index => $backend ) {
197 if ( $index === $this->masterIndex
) {
198 continue; // done already
201 $realOps = $this->substOpBatchPaths( $ops, $backend );
202 if ( $this->asyncWrites
&& !$this->hasVolatileSources( $ops ) ) {
203 // Bind $scopeLock to the callback to preserve locks
204 $this->callNowOrLater(
206 $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname
208 $this->logger
->debug(
209 "$fname: '{$backend->getName()}' async replication; paths: " .
210 implode( ', ', $relevantPaths )
212 $backend->doOperations( $realOps, $opts );
216 $this->logger
->debug(
217 "$fname: '{$backend->getName()}' sync replication; paths: " .
218 implode( ', ', $relevantPaths )
220 $status->merge( $backend->doOperations( $realOps, $opts ) );
224 // Make 'success', 'successCount', and 'failCount' fields reflect
225 // the overall operation, rather than all the batches for each backend.
226 // Do this by only using success values from the master backend's batch.
227 $status->success
= $masterStatus->success
;
228 $status->successCount
= $masterStatus->successCount
;
229 $status->failCount
= $masterStatus->failCount
;
235 * Check that a set of files are consistent across all internal backends
237 * This method should only be called if the files are locked or the backend
238 * is in read-only mode
240 * @param string[] $paths List of storage paths
241 * @return StatusValue
243 public function consistencyCheck( array $paths ) {
244 $status = $this->newStatus();
245 if ( $this->syncChecks
== 0 ||
count( $this->backends
) <= 1 ) {
246 return $status; // skip checks
249 // Preload all of the stat info in as few round trips as possible
250 foreach ( $this->backends
as $backend ) {
251 $realPaths = $this->substPaths( $paths, $backend );
252 $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] );
255 foreach ( $paths as $path ) {
256 $params = [ 'src' => $path, 'latest' => true ];
257 // Get the state of the file on the master backend
258 $masterBackend = $this->backends
[$this->masterIndex
];
259 $masterParams = $this->substOpPaths( $params, $masterBackend );
260 $masterStat = $masterBackend->getFileStat( $masterParams );
261 if ( $masterStat === self
::STAT_ERROR
) {
262 $status->fatal( 'backend-fail-stat', $path );
265 if ( $this->syncChecks
& self
::CHECK_SHA1
) {
266 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
267 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
268 $status->fatal( 'backend-fail-hash', $path );
272 $masterSha1 = null; // unused
275 // Check if all clone backends agree with the master...
276 foreach ( $this->backends
as $index => $cloneBackend ) {
277 if ( $index === $this->masterIndex
) {
281 // Get the state of the file on the clone backend
282 $cloneParams = $this->substOpPaths( $params, $cloneBackend );
283 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
286 // File exists in the master backend
288 // File is missing from the clone backend
289 $status->fatal( 'backend-fail-synced', $path );
291 ( $this->syncChecks
& self
::CHECK_SIZE
) &&
292 $cloneStat['size'] !== $masterStat['size']
294 // File in the clone backend is different
295 $status->fatal( 'backend-fail-synced', $path );
297 ( $this->syncChecks
& self
::CHECK_TIME
) &&
299 (int)ConvertibleTimestamp
::convert( TS_UNIX
, $masterStat['mtime'] ) -
300 (int)ConvertibleTimestamp
::convert( TS_UNIX
, $cloneStat['mtime'] )
303 // File in the clone backend is significantly newer or older
304 $status->fatal( 'backend-fail-synced', $path );
306 ( $this->syncChecks
& self
::CHECK_SHA1
) &&
307 $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1
309 // File in the clone backend is different
310 $status->fatal( 'backend-fail-synced', $path );
313 // File does not exist in the master backend
315 // Stray file exists in the clone backend
316 $status->fatal( 'backend-fail-synced', $path );
326 * Check that a set of file paths are usable across all internal backends
328 * @param string[] $paths List of storage paths
329 * @return StatusValue
331 public function accessibilityCheck( array $paths ) {
332 $status = $this->newStatus();
333 if ( count( $this->backends
) <= 1 ) {
334 return $status; // skip checks
337 foreach ( $paths as $path ) {
338 foreach ( $this->backends
as $backend ) {
339 $realPath = $this->substPaths( $path, $backend );
340 if ( !$backend->isPathUsableInternal( $realPath ) ) {
341 $status->fatal( 'backend-fail-usable', $path );
350 * Check that a set of files are consistent across all internal backends
351 * and re-synchronize those files against the "multi master" if needed.
353 * This method should only be called if the files are locked
355 * @param string[] $paths List of storage paths
356 * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
357 * @return StatusValue
359 public function resyncFiles( array $paths, $resyncMode = true ) {
360 $status = $this->newStatus();
363 foreach ( $paths as $path ) {
364 $params = [ 'src' => $path, 'latest' => true ];
365 // Get the state of the file on the master backend
366 $masterBackend = $this->backends
[$this->masterIndex
];
367 $masterParams = $this->substOpPaths( $params, $masterBackend );
368 $masterPath = $masterParams['src'];
369 $masterStat = $masterBackend->getFileStat( $masterParams );
370 if ( $masterStat === self
::STAT_ERROR
) {
371 $status->fatal( 'backend-fail-stat', $path );
372 $this->logger
->error( "$fname: file '$masterPath' is not available" );
375 $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
376 if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
377 $status->fatal( 'backend-fail-hash', $path );
378 $this->logger
->error( "$fname: file '$masterPath' hash does not match stat" );
382 // Check of all clone backends agree with the master...
383 foreach ( $this->backends
as $index => $cloneBackend ) {
384 if ( $index === $this->masterIndex
) {
388 // Get the state of the file on the clone backend
389 $cloneParams = $this->substOpPaths( $params, $cloneBackend );
390 $clonePath = $cloneParams['src'];
391 $cloneStat = $cloneBackend->getFileStat( $cloneParams );
392 if ( $cloneStat === self
::STAT_ERROR
) {
393 $status->fatal( 'backend-fail-stat', $path );
394 $this->logger
->error( "$fname: file '$clonePath' is not available" );
397 $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams );
398 if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) {
399 $status->fatal( 'backend-fail-hash', $path );
400 $this->logger
->error( "$fname: file '$clonePath' hash does not match stat" );
404 if ( $masterSha1 === $cloneSha1 ) {
405 // File is either the same in both backends or absent from both backends
406 $this->logger
->debug( "$fname: file '$clonePath' matches '$masterPath'" );
407 } elseif ( $masterSha1 !== false ) {
408 // File is either missing from or different in the clone backend
410 $resyncMode === 'conservative' &&
412 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
413 $cloneStat['mtime'] > $masterStat['mtime']
415 // Do not replace files with older ones; reduces the risk of data loss
416 $status->fatal( 'backend-fail-synced', $path );
418 // Copy the master backend file to the clone backend in overwrite mode
419 $fsFile = $masterBackend->getLocalReference( $masterParams );
420 $status->merge( $cloneBackend->quickStore( [
425 } elseif ( $masterStat === false ) {
426 // Stray file exists in the clone backend
427 if ( $resyncMode === 'conservative' ) {
428 // Do not delete stray files; reduces the risk of data loss
429 $status->fatal( 'backend-fail-synced', $path );
430 $this->logger
->error( "$fname: not allowed to delete file '$clonePath'" );
432 // Delete the stay file from the clone backend
433 $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) );
439 if ( !$status->isOK() ) {
440 $this->logger
->error( "$fname: failed to resync: " . implode( ', ', $paths ) );
447 * Get a list of file storage paths to read or write for a list of operations
449 * @param array $ops Same format as doOperations()
450 * @return array List of storage paths to files (does not include directories)
452 protected function fileStoragePathsForOps( array $ops ) {
454 foreach ( $ops as $op ) {
455 if ( isset( $op['src'] ) ) {
456 // For things like copy/move/delete with "ignoreMissingSource" and there
457 // is no source file, nothing should happen and there should be no errors.
458 if ( empty( $op['ignoreMissingSource'] )
459 ||
$this->fileExists( [ 'src' => $op['src'] ] )
461 $paths[] = $op['src'];
464 if ( isset( $op['srcs'] ) ) {
465 $paths = array_merge( $paths, $op['srcs'] );
467 if ( isset( $op['dst'] ) ) {
468 $paths[] = $op['dst'];
472 return array_values( array_unique( array_filter( $paths, [ FileBackend
::class, 'isStoragePath' ] ) ) );
476 * Substitute the backend name in storage path parameters
477 * for a set of operations with that of a given internal backend.
479 * @param array $ops List of file operation arrays
480 * @param FileBackendStore $backend
483 protected function substOpBatchPaths( array $ops, FileBackendStore
$backend ) {
484 $newOps = []; // operations
485 foreach ( $ops as $op ) {
486 $newOp = $op; // operation
487 foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
488 if ( isset( $newOp[$par] ) ) { // string or array
489 $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
499 * Same as substOpBatchPaths() but for a single operation
501 * @param array $ops File operation array
502 * @param FileBackendStore $backend
505 protected function substOpPaths( array $ops, FileBackendStore
$backend ) {
506 $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
512 * Substitute the backend of storage paths with an internal backend's name
514 * @param string[]|string $paths List of paths or single string path
515 * @param FileBackendStore $backend
516 * @return string[]|string
518 protected function substPaths( $paths, FileBackendStore
$backend ) {
520 '!^mwstore://' . preg_quote( $this->name
, '!' ) . '/!',
521 StringUtils
::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
522 $paths // string or array
527 * Substitute the backend of internal storage paths with the proxy backend's name
529 * @param string[]|string $paths List of paths or single string path
530 * @param FileBackendStore $backend internal storage backend
531 * @return string[]|string
533 protected function unsubstPaths( $paths, FileBackendStore
$backend ) {
535 '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!',
536 StringUtils
::escapeRegexReplacement( "mwstore://{$this->name}/" ),
537 $paths // string or array
542 * @param array[] $ops File operations for FileBackend::doOperations()
543 * @return bool Whether there are file path sources with outside lifetime/ownership
545 protected function hasVolatileSources( array $ops ) {
546 foreach ( $ops as $op ) {
547 if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
548 return true; // source file might be deleted anytime after do*Operations()
555 protected function doQuickOperationsInternal( array $ops, array $opts ) {
556 $status = $this->newStatus();
557 // Do the operations on the master backend; setting StatusValue fields
558 $realOps = $this->substOpBatchPaths( $ops, $this->backends
[$this->masterIndex
] );
559 $masterStatus = $this->backends
[$this->masterIndex
]->doQuickOperations( $realOps );
560 $status->merge( $masterStatus );
561 // Propagate the operations to the clone backends...
562 foreach ( $this->backends
as $index => $backend ) {
563 if ( $index === $this->masterIndex
) {
564 continue; // done already
567 $realOps = $this->substOpBatchPaths( $ops, $backend );
568 if ( $this->asyncWrites
&& !$this->hasVolatileSources( $ops ) ) {
569 $this->callNowOrLater(
570 static function () use ( $backend, $realOps ) {
571 $backend->doQuickOperations( $realOps );
575 $status->merge( $backend->doQuickOperations( $realOps ) );
578 // Make 'success', 'successCount', and 'failCount' fields reflect
579 // the overall operation, rather than all the batches for each backend.
580 // Do this by only using success values from the master backend's batch.
581 $status->success
= $masterStatus->success
;
582 $status->successCount
= $masterStatus->successCount
;
583 $status->failCount
= $masterStatus->failCount
;
588 protected function doPrepare( array $params ) {
589 return $this->doDirectoryOp( 'prepare', $params );
592 protected function doSecure( array $params ) {
593 return $this->doDirectoryOp( 'secure', $params );
596 protected function doPublish( array $params ) {
597 return $this->doDirectoryOp( 'publish', $params );
600 protected function doClean( array $params ) {
601 return $this->doDirectoryOp( 'clean', $params );
605 * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
606 * @param array $params Method arguments
607 * @return StatusValue
609 protected function doDirectoryOp( $method, array $params ) {
610 $status = $this->newStatus();
612 $realParams = $this->substOpPaths( $params, $this->backends
[$this->masterIndex
] );
613 $masterStatus = $this->backends
[$this->masterIndex
]->$method( $realParams );
614 $status->merge( $masterStatus );
616 foreach ( $this->backends
as $index => $backend ) {
617 if ( $index === $this->masterIndex
) {
618 continue; // already done
621 $realParams = $this->substOpPaths( $params, $backend );
622 if ( $this->asyncWrites
) {
623 $this->callNowOrLater(
624 static function () use ( $backend, $method, $realParams ) {
625 $backend->$method( $realParams );
629 $status->merge( $backend->$method( $realParams ) );
636 public function concatenate( array $params ) {
637 $status = $this->newStatus();
638 // We are writing to an FS file, so we don't need to do this per-backend
639 $index = $this->getReadIndexFromParams( $params );
640 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
642 $status->merge( $this->backends
[$index]->concatenate( $realParams ) );
647 public function fileExists( array $params ) {
648 $index = $this->getReadIndexFromParams( $params );
649 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
651 return $this->backends
[$index]->fileExists( $realParams );
654 public function getFileTimestamp( array $params ) {
655 $index = $this->getReadIndexFromParams( $params );
656 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
658 return $this->backends
[$index]->getFileTimestamp( $realParams );
661 public function getFileSize( array $params ) {
662 $index = $this->getReadIndexFromParams( $params );
663 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
665 return $this->backends
[$index]->getFileSize( $realParams );
668 public function getFileStat( array $params ) {
669 $index = $this->getReadIndexFromParams( $params );
670 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
672 return $this->backends
[$index]->getFileStat( $realParams );
675 public function getFileXAttributes( array $params ) {
676 $index = $this->getReadIndexFromParams( $params );
677 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
679 return $this->backends
[$index]->getFileXAttributes( $realParams );
682 public function getFileContentsMulti( array $params ) {
683 $index = $this->getReadIndexFromParams( $params );
684 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
686 $contentsM = $this->backends
[$index]->getFileContentsMulti( $realParams );
688 $contents = []; // (path => FSFile) mapping using the proxy backend's name
689 foreach ( $contentsM as $path => $data ) {
690 $contents[$this->unsubstPaths( $path, $this->backends
[$index] )] = $data;
696 public function getFileSha1Base36( array $params ) {
697 $index = $this->getReadIndexFromParams( $params );
698 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
700 return $this->backends
[$index]->getFileSha1Base36( $realParams );
703 public function getFileProps( array $params ) {
704 $index = $this->getReadIndexFromParams( $params );
705 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
707 return $this->backends
[$index]->getFileProps( $realParams );
710 public function streamFile( array $params ) {
711 $index = $this->getReadIndexFromParams( $params );
712 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
714 return $this->backends
[$index]->streamFile( $realParams );
717 public function getLocalReferenceMulti( array $params ) {
718 $index = $this->getReadIndexFromParams( $params );
719 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
721 $fsFilesM = $this->backends
[$index]->getLocalReferenceMulti( $realParams );
723 $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
724 foreach ( $fsFilesM as $path => $fsFile ) {
725 $fsFiles[$this->unsubstPaths( $path, $this->backends
[$index] )] = $fsFile;
731 public function getLocalCopyMulti( array $params ) {
732 $index = $this->getReadIndexFromParams( $params );
733 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
735 $tempFilesM = $this->backends
[$index]->getLocalCopyMulti( $realParams );
737 $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
738 foreach ( $tempFilesM as $path => $tempFile ) {
739 $tempFiles[$this->unsubstPaths( $path, $this->backends
[$index] )] = $tempFile;
745 public function getFileHttpUrl( array $params ) {
746 $index = $this->getReadIndexFromParams( $params );
747 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
749 return $this->backends
[$index]->getFileHttpUrl( $realParams );
752 public function addShellboxInputFile( BoxedCommand
$command, string $boxedName,
755 $index = $this->getReadIndexFromParams( $params );
756 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
757 return $this->backends
[$index]->addShellboxInputFile( $command, $boxedName, $realParams );
760 public function directoryExists( array $params ) {
761 $realParams = $this->substOpPaths( $params, $this->backends
[$this->masterIndex
] );
763 return $this->backends
[$this->masterIndex
]->directoryExists( $realParams );
766 public function getDirectoryList( array $params ) {
767 $realParams = $this->substOpPaths( $params, $this->backends
[$this->masterIndex
] );
769 return $this->backends
[$this->masterIndex
]->getDirectoryList( $realParams );
772 public function getFileList( array $params ) {
773 if ( isset( $params['forWrite'] ) && $params['forWrite'] ) {
774 return $this->getFileListForWrite( $params );
777 $realParams = $this->substOpPaths( $params, $this->backends
[$this->masterIndex
] );
778 return $this->backends
[$this->masterIndex
]->getFileList( $realParams );
781 private function getFileListForWrite( $params ) {
783 // Get the list of thumbnails from all backends to allow
784 // deleting all of them. Otherwise, old thumbnails existing on
785 // one backend only won't get updated in reupload (T331138).
786 foreach ( $this->backends
as $backend ) {
787 $realParams = $this->substOpPaths( $params, $backend );
788 $iterator = $backend->getFileList( $realParams );
789 if ( $iterator !== null ) {
790 foreach ( $iterator as $file ) {
796 return array_unique( $files );
799 public function getFeatures() {
800 return $this->backends
[$this->masterIndex
]->getFeatures();
803 public function clearCache( ?
array $paths = null ) {
804 foreach ( $this->backends
as $backend ) {
805 $realPaths = is_array( $paths ) ?
$this->substPaths( $paths, $backend ) : null;
806 $backend->clearCache( $realPaths );
810 public function preloadCache( array $paths ) {
811 $realPaths = $this->substPaths( $paths, $this->backends
[$this->readIndex
] );
812 $this->backends
[$this->readIndex
]->preloadCache( $realPaths );
815 public function preloadFileStat( array $params ) {
816 $index = $this->getReadIndexFromParams( $params );
817 $realParams = $this->substOpPaths( $params, $this->backends
[$index] );
819 return $this->backends
[$index]->preloadFileStat( $realParams );
822 public function getScopedLocksForOps( array $ops, StatusValue
$status ) {
823 $realOps = $this->substOpBatchPaths( $ops, $this->backends
[$this->masterIndex
] );
824 $fileOps = $this->backends
[$this->masterIndex
]->getOperationsInternal( $realOps );
825 // Get the paths to lock from the master backend
826 $paths = $this->backends
[$this->masterIndex
]->getPathsToLockForOpsInternal( $fileOps );
827 // Get the paths under the proxy backend's name
829 LockManager
::LOCK_UW
=> $this->unsubstPaths(
830 $paths[LockManager
::LOCK_UW
],
831 $this->backends
[$this->masterIndex
]
833 LockManager
::LOCK_EX
=> $this->unsubstPaths(
834 $paths[LockManager
::LOCK_EX
],
835 $this->backends
[$this->masterIndex
]
839 // Actually acquire the locks
840 return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
844 * @param array $params
845 * @return int The master or read affinity backend index, based on $params['latest']
847 protected function getReadIndexFromParams( array $params ) {
848 return !empty( $params['latest'] ) ?
$this->masterIndex
: $this->readIndex
;
852 /** @deprecated class alias since 1.43 */
853 class_alias( FileBackendMultiWrite
::class, 'FileBackendMultiWrite' );