Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.86% covered (warning)
68.86%
199 / 289
74.36% covered (warning)
74.36%
29 / 39
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileBackendMultiWrite
69.10% covered (warning)
69.10%
199 / 288
74.36% covered (warning)
74.36%
29 / 39
388.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
84.62% covered (warning)
84.62%
22 / 26
0.00% covered (danger)
0.00%
0 / 1
10.36
 doOperationsInternal
70.00% covered (warning)
70.00%
28 / 40
0.00% covered (danger)
0.00%
0 / 1
11.19
 resyncFiles
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
272
 fileStoragePathsForOps
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 substOpBatchPaths
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 substOpPaths
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 substPaths
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 unsubstPaths
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 hasVolatileSources
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 doQuickOperationsInternal
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 doPrepare
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doSecure
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doPublish
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doClean
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doDirectoryOp
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
4.49
 concatenate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 fileExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileTimestamp
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileSize
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileStat
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileXAttributes
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getFileContentsMulti
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getFileSha1Base36
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFileProps
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 streamFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getLocalReferenceMulti
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getLocalCopyMulti
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getFileHttpUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addShellboxInputFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 directoryExists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDirectoryList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFileList
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getFileListForWrite
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 preloadCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 preloadFileStat
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getScopedLocksForOps
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 getReadIndexFromParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Proxy backend that mirrors writes to several internal backends.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup FileBackend
8 */
9
10namespace Wikimedia\FileBackend;
11
12use InvalidArgumentException;
13use LogicException;
14use Shellbox\Command\BoxedCommand;
15use StatusValue;
16use Wikimedia\LockManager\LockManager;
17use Wikimedia\StringUtils\StringUtils;
18
19/**
20 * @brief Proxy backend that mirrors writes to several internal backends.
21 *
22 * This class defines a multi-write backend. Multiple backends can be
23 * registered to this proxy backend and it will act as a single backend.
24 * Use this when all access to those backends is through this proxy backend.
25 * At least one of the backends must be declared the "master" backend.
26 *
27 * Only use this class when transitioning from one storage system to another.
28 *
29 * Read operations are only done on the 'master' backend for consistency.
30 * Except on getting list of thumbnails for write operations.
31 * Write operations are performed on all backends, starting with the master.
32 * This makes a best-effort to have transactional semantics.
33 *
34 * @ingroup FileBackend
35 * @since 1.19
36 */
37class FileBackendMultiWrite extends FileBackend {
38    /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
39    protected $backends = [];
40
41    /** @var int Index of master backend */
42    protected $masterIndex = -1;
43    /** @var int Index of read affinity backend */
44    protected $readIndex = -1;
45
46    /** @var bool */
47    protected $asyncWrites = false;
48
49    /** @var int Compare file sizes among backends */
50    private const CHECK_SIZE = 1;
51    /** @var int Compare file mtimes among backends */
52    private const CHECK_TIME = 2;
53    /** @var int Compare file hashes among backends */
54    private const CHECK_SHA1 = 4;
55
56    /**
57     * Construct a proxy backend that consists of several internal backends.
58     * Locking and read-only checks are handled by the proxy backend.
59     *
60     * Additional $config params include:
61     *   - backends       : Array of backend config and multi-backend settings.
62     *                      Each value is the config used in the constructor of a
63     *                      FileBackendStore class, but with these additional settings:
64     *                        - class         : The name of the backend class
65     *                        - isMultiMaster : This must be set for one backend.
66     *                        - readAffinity  : Use this for reads without 'latest' set.
67     *   - replication    : Set to 'async' to defer file operations on the non-master backends.
68     *                      This will apply such updates post-send for web requests.
69     */
70    public function __construct( array $config ) {
71        parent::__construct( $config );
72        $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
73        // Construct backends here rather than via registration
74        // to keep these backends hidden from outside the proxy.
75        $namesUsed = [];
76        foreach ( $config['backends'] as $index => $beConfig ) {
77            $name = $beConfig['name'];
78            if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
79                throw new LogicException( "Two or more backends defined with the name $name." );
80            }
81            $namesUsed[$name] = 1;
82            // Alter certain sub-backend settings
83            unset( $beConfig['readOnly'] ); // use proxy backend setting
84            unset( $beConfig['lockManager'] ); // lock under proxy backend
85            $beConfig['domainId'] = $this->domainId; // use the proxy backend wiki ID
86            $beConfig['logger'] = $this->logger; // use the proxy backend logger
87            if ( !empty( $beConfig['isMultiMaster'] ) ) {
88                if ( $this->masterIndex >= 0 ) {
89                    throw new LogicException( 'More than one master backend defined.' );
90                }
91                $this->masterIndex = $index; // this is the "master"
92            }
93            if ( !empty( $beConfig['readAffinity'] ) ) {
94                $this->readIndex = $index; // prefer this for reads
95            }
96            // Create sub-backend object
97            if ( !isset( $beConfig['class'] ) ) {
98                throw new InvalidArgumentException( 'No class given for a backend config.' );
99            }
100            $class = $beConfig['class'];
101            $this->backends[$index] = new $class( $beConfig );
102        }
103        if ( $this->masterIndex < 0 ) { // need backends and must have a master
104            throw new LogicException( 'No master backend defined.' );
105        }
106        if ( $this->readIndex < 0 ) {
107            $this->readIndex = $this->masterIndex; // default
108        }
109    }
110
111    /** @inheritDoc */
112    final protected function doOperationsInternal( array $ops, array $opts ) {
113        $status = $this->newStatus();
114
115        $fname = __METHOD__;
116        $mbe = $this->backends[$this->masterIndex]; // convenience
117
118        // Acquire any locks as needed
119        $scopeLock = null;
120        if ( empty( $opts['nonLocking'] ) ) {
121            $scopeLock = $this->getScopedLocksForOps( $ops, $status );
122            if ( !$status->isOK() ) {
123                return $status; // abort
124            }
125        }
126        // Get the list of paths to read/write
127        $relevantPaths = $this->fileStoragePathsForOps( $ops );
128        // Clear any cache entries (after locks acquired)
129        $this->clearCache( $relevantPaths );
130        $opts['preserveCache'] = true;
131        // Actually attempt the operation batch on the master backend
132        $realOps = $this->substOpBatchPaths( $ops, $mbe );
133        $masterStatus = $mbe->doOperations( $realOps, $opts );
134        $status->merge( $masterStatus );
135        // Propagate the operations to the clone backends if there were no unexpected errors
136        // and everything didn't fail due to predicted errors. If $ops only had one operation,
137        // this might avoid backend sync inconsistencies.
138        if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
139            foreach ( $this->backends as $index => $backend ) {
140                if ( $index === $this->masterIndex ) {
141                    continue; // done already
142                }
143
144                $realOps = $this->substOpBatchPaths( $ops, $backend );
145                if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
146                    // Bind $scopeLock to the callback to preserve locks
147                    $this->callNowOrLater(
148                        function () use (
149                            // @phan-suppress-next-line PhanUnusedClosureUseVariable
150                            $backend, $realOps, $opts, $scopeLock, $relevantPaths, $fname
151                        ) {
152                            $this->logger->debug(
153                                "$fname: '{$backend->getName()}' async replication; paths: " .
154                                implode( ', ', $relevantPaths )
155                            );
156                            $backend->doOperations( $realOps, $opts );
157                        }
158                    );
159                } else {
160                    $this->logger->debug(
161                        "$fname: '{$backend->getName()}' sync replication; paths: " .
162                        implode( ', ', $relevantPaths )
163                    );
164                    $status->merge( $backend->doOperations( $realOps, $opts ) );
165                }
166            }
167        }
168        // Make 'success', 'successCount', and 'failCount' fields reflect
169        // the overall operation, rather than all the batches for each backend.
170        // Do this by only using success values from the master backend's batch.
171        $status->success = $masterStatus->success;
172        $status->successCount = $masterStatus->successCount;
173        $status->failCount = $masterStatus->failCount;
174
175        return $status;
176    }
177
178    /**
179     * Check that a set of files are consistent across all internal backends
180     * and re-synchronize those files against the "multi master" if needed.
181     *
182     * This method should only be called if the files are locked
183     *
184     * @param string[] $paths List of storage paths
185     * @param string|bool $resyncMode False, True, or "conservative"; see __construct()
186     * @return StatusValue
187     */
188    public function resyncFiles( array $paths, $resyncMode = true ) {
189        $status = $this->newStatus();
190
191        $fname = __METHOD__;
192        foreach ( $paths as $path ) {
193            $params = [ 'src' => $path, 'latest' => true ];
194            // Get the state of the file on the master backend
195            $masterBackend = $this->backends[$this->masterIndex];
196            $masterParams = $this->substOpPaths( $params, $masterBackend );
197            $masterPath = $masterParams['src'];
198            $masterStat = $masterBackend->getFileStat( $masterParams );
199            if ( $masterStat === self::STAT_ERROR ) {
200                $status->fatal( 'backend-fail-stat', $path );
201                $this->logger->error( "$fname: file '$masterPath' is not available" );
202                continue;
203            }
204            $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams );
205            if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) {
206                $status->fatal( 'backend-fail-hash', $path );
207                $this->logger->error( "$fname: file '$masterPath' hash does not match stat" );
208                continue;
209            }
210
211            // Check of all clone backends agree with the master...
212            foreach ( $this->backends as $index => $cloneBackend ) {
213                if ( $index === $this->masterIndex ) {
214                    continue; // master
215                }
216
217                // Get the state of the file on the clone backend
218                $cloneParams = $this->substOpPaths( $params, $cloneBackend );
219                $clonePath = $cloneParams['src'];
220                $cloneStat = $cloneBackend->getFileStat( $cloneParams );
221                if ( $cloneStat === self::STAT_ERROR ) {
222                    $status->fatal( 'backend-fail-stat', $path );
223                    $this->logger->error( "$fname: file '$clonePath' is not available" );
224                    continue;
225                }
226                $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams );
227                if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) {
228                    $status->fatal( 'backend-fail-hash', $path );
229                    $this->logger->error( "$fname: file '$clonePath' hash does not match stat" );
230                    continue;
231                }
232
233                if ( $masterSha1 === $cloneSha1 ) {
234                    // File is either the same in both backends or absent from both backends
235                    $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" );
236                } elseif ( $masterSha1 !== false ) {
237                    // File is either missing from or different in the clone backend
238                    if (
239                        $resyncMode === 'conservative' &&
240                        $cloneStat &&
241                        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
242                        $cloneStat['mtime'] > $masterStat['mtime']
243                    ) {
244                        // Do not replace files with older ones; reduces the risk of data loss
245                        $status->fatal( 'backend-fail-synced', $path );
246                    } else {
247                        // Copy the master backend file to the clone backend in overwrite mode
248                        $fsFile = $masterBackend->getLocalReference( $masterParams );
249                        $status->merge( $cloneBackend->quickStore( [
250                            'src' => $fsFile,
251                            'dst' => $clonePath
252                        ] ) );
253                    }
254                } elseif ( $masterStat === false ) {
255                    // Stray file exists in the clone backend
256                    if ( $resyncMode === 'conservative' ) {
257                        // Do not delete stray files; reduces the risk of data loss
258                        $status->fatal( 'backend-fail-synced', $path );
259                        $this->logger->error( "$fname: not allowed to delete file '$clonePath'" );
260                    } else {
261                        // Delete the stay file from the clone backend
262                        $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) );
263                    }
264                }
265            }
266        }
267
268        if ( !$status->isOK() ) {
269            $this->logger->error( "$fname: failed to resync: " . implode( ', ', $paths ) );
270        }
271
272        return $status;
273    }
274
275    /**
276     * Get a list of file storage paths to read or write for a list of operations
277     *
278     * @param array $ops Same format as doOperations()
279     * @return array List of storage paths to files (does not include directories)
280     */
281    protected function fileStoragePathsForOps( array $ops ) {
282        $paths = [];
283        foreach ( $ops as $op ) {
284            if ( isset( $op['src'] ) ) {
285                $paths[] = $op['src'];
286            }
287            if ( isset( $op['srcs'] ) ) {
288                $paths = array_merge( $paths, $op['srcs'] );
289            }
290            if ( isset( $op['dst'] ) ) {
291                $paths[] = $op['dst'];
292            }
293        }
294
295        return array_values( array_unique( array_filter(
296            $paths,
297            FileBackend::isStoragePath( ... )
298        ) ) );
299    }
300
301    /**
302     * Substitute the backend name in storage path parameters
303     * for a set of operations with that of a given internal backend.
304     *
305     * @param array $ops List of file operation arrays
306     * @param FileBackendStore $backend
307     * @return array
308     */
309    protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
310        $newOps = []; // operations
311        foreach ( $ops as $op ) {
312            $newOp = $op; // operation
313            foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
314                if ( isset( $newOp[$par] ) ) { // string or array
315                    $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
316                }
317            }
318            $newOps[] = $newOp;
319        }
320
321        return $newOps;
322    }
323
324    /**
325     * Same as substOpBatchPaths() but for a single operation
326     *
327     * @param array $ops File operation array
328     * @param FileBackendStore $backend
329     * @return array
330     */
331    protected function substOpPaths( array $ops, FileBackendStore $backend ) {
332        $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
333
334        return $newOps[0];
335    }
336
337    /**
338     * Substitute the backend of storage paths with an internal backend's name
339     *
340     * @param string[]|string $paths List of paths or single string path
341     * @param FileBackendStore $backend
342     * @return string[]|string
343     */
344    protected function substPaths( $paths, FileBackendStore $backend ) {
345        return preg_replace(
346            '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!',
347            StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
348            $paths // string or array
349        );
350    }
351
352    /**
353     * Substitute the backend of internal storage paths with the proxy backend's name
354     *
355     * @param string[]|string $paths List of paths or single string path
356     * @param FileBackendStore $backend internal storage backend
357     * @return string[]|string
358     */
359    protected function unsubstPaths( $paths, FileBackendStore $backend ) {
360        return preg_replace(
361            '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!',
362            StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ),
363            $paths // string or array
364        );
365    }
366
367    /**
368     * @param array[] $ops File operations for FileBackend::doOperations()
369     * @return bool Whether there are file path sources with outside lifetime/ownership
370     */
371    protected function hasVolatileSources( array $ops ) {
372        foreach ( $ops as $op ) {
373            if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
374                return true; // source file might be deleted anytime after do*Operations()
375            }
376        }
377
378        return false;
379    }
380
381    /** @inheritDoc */
382    protected function doQuickOperationsInternal( array $ops, array $opts ) {
383        $status = $this->newStatus();
384        // Do the operations on the master backend; setting StatusValue fields
385        $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
386        $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
387        $status->merge( $masterStatus );
388        // Propagate the operations to the clone backends...
389        foreach ( $this->backends as $index => $backend ) {
390            if ( $index === $this->masterIndex ) {
391                continue; // done already
392            }
393
394            $realOps = $this->substOpBatchPaths( $ops, $backend );
395            if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
396                $this->callNowOrLater(
397                    static function () use ( $backend, $realOps ) {
398                        $backend->doQuickOperations( $realOps );
399                    }
400                );
401            } else {
402                $status->merge( $backend->doQuickOperations( $realOps ) );
403            }
404        }
405        // Make 'success', 'successCount', and 'failCount' fields reflect
406        // the overall operation, rather than all the batches for each backend.
407        // Do this by only using success values from the master backend's batch.
408        $status->success = $masterStatus->success;
409        $status->successCount = $masterStatus->successCount;
410        $status->failCount = $masterStatus->failCount;
411
412        return $status;
413    }
414
415    /** @inheritDoc */
416    protected function doPrepare( array $params ) {
417        return $this->doDirectoryOp( 'prepare', $params );
418    }
419
420    /** @inheritDoc */
421    protected function doSecure( array $params ) {
422        return $this->doDirectoryOp( 'secure', $params );
423    }
424
425    /** @inheritDoc */
426    protected function doPublish( array $params ) {
427        return $this->doDirectoryOp( 'publish', $params );
428    }
429
430    /** @inheritDoc */
431    protected function doClean( array $params ) {
432        return $this->doDirectoryOp( 'clean', $params );
433    }
434
435    /**
436     * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
437     * @param array $params Method arguments
438     * @return StatusValue
439     */
440    protected function doDirectoryOp( $method, array $params ) {
441        $status = $this->newStatus();
442
443        $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
444        $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
445        $status->merge( $masterStatus );
446
447        foreach ( $this->backends as $index => $backend ) {
448            if ( $index === $this->masterIndex ) {
449                continue; // already done
450            }
451
452            $realParams = $this->substOpPaths( $params, $backend );
453            if ( $this->asyncWrites ) {
454                $this->callNowOrLater(
455                    static function () use ( $backend, $method, $realParams ) {
456                        $backend->$method( $realParams );
457                    }
458                );
459            } else {
460                $status->merge( $backend->$method( $realParams ) );
461            }
462        }
463
464        return $status;
465    }
466
467    /** @inheritDoc */
468    public function concatenate( array $params ) {
469        $status = $this->newStatus();
470        // We are writing to an FS file, so we don't need to do this per-backend
471        $index = $this->getReadIndexFromParams( $params );
472        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
473
474        $status->merge( $this->backends[$index]->concatenate( $realParams ) );
475
476        return $status;
477    }
478
479    /** @inheritDoc */
480    public function fileExists( array $params ) {
481        $index = $this->getReadIndexFromParams( $params );
482        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
483
484        return $this->backends[$index]->fileExists( $realParams );
485    }
486
487    /** @inheritDoc */
488    public function getFileTimestamp( array $params ) {
489        $index = $this->getReadIndexFromParams( $params );
490        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
491
492        return $this->backends[$index]->getFileTimestamp( $realParams );
493    }
494
495    /** @inheritDoc */
496    public function getFileSize( array $params ) {
497        $index = $this->getReadIndexFromParams( $params );
498        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
499
500        return $this->backends[$index]->getFileSize( $realParams );
501    }
502
503    /** @inheritDoc */
504    public function getFileStat( array $params ) {
505        $index = $this->getReadIndexFromParams( $params );
506        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
507
508        return $this->backends[$index]->getFileStat( $realParams );
509    }
510
511    /** @inheritDoc */
512    public function getFileXAttributes( array $params ) {
513        $index = $this->getReadIndexFromParams( $params );
514        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
515
516        return $this->backends[$index]->getFileXAttributes( $realParams );
517    }
518
519    /** @inheritDoc */
520    public function getFileContentsMulti( array $params ) {
521        $index = $this->getReadIndexFromParams( $params );
522        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
523
524        $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
525
526        $contents = []; // (path => FSFile) mapping using the proxy backend's name
527        foreach ( $contentsM as $path => $data ) {
528            $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data;
529        }
530
531        return $contents;
532    }
533
534    /** @inheritDoc */
535    public function getFileSha1Base36( array $params ) {
536        $index = $this->getReadIndexFromParams( $params );
537        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
538
539        return $this->backends[$index]->getFileSha1Base36( $realParams );
540    }
541
542    /** @inheritDoc */
543    public function getFileProps( array $params ) {
544        $index = $this->getReadIndexFromParams( $params );
545        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
546
547        return $this->backends[$index]->getFileProps( $realParams );
548    }
549
550    /** @inheritDoc */
551    public function streamFile( array $params ) {
552        $index = $this->getReadIndexFromParams( $params );
553        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
554
555        return $this->backends[$index]->streamFile( $realParams );
556    }
557
558    /** @inheritDoc */
559    public function getLocalReferenceMulti( array $params ) {
560        $index = $this->getReadIndexFromParams( $params );
561        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
562
563        $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
564
565        $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
566        foreach ( $fsFilesM as $path => $fsFile ) {
567            $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile;
568        }
569
570        return $fsFiles;
571    }
572
573    /** @inheritDoc */
574    public function getLocalCopyMulti( array $params ) {
575        $index = $this->getReadIndexFromParams( $params );
576        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
577
578        $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
579
580        $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
581        foreach ( $tempFilesM as $path => $tempFile ) {
582            $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile;
583        }
584
585        return $tempFiles;
586    }
587
588    /** @inheritDoc */
589    public function getFileHttpUrl( array $params ) {
590        $index = $this->getReadIndexFromParams( $params );
591        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
592
593        return $this->backends[$index]->getFileHttpUrl( $realParams );
594    }
595
596    /** @inheritDoc */
597    public function addShellboxInputFile( BoxedCommand $command, string $boxedName,
598        array $params
599    ) {
600        $index = $this->getReadIndexFromParams( $params );
601        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
602        return $this->backends[$index]->addShellboxInputFile( $command, $boxedName, $realParams );
603    }
604
605    /** @inheritDoc */
606    public function directoryExists( array $params ) {
607        $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
608
609        return $this->backends[$this->masterIndex]->directoryExists( $realParams );
610    }
611
612    /** @inheritDoc */
613    public function getDirectoryList( array $params ) {
614        $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
615
616        return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
617    }
618
619    /** @inheritDoc */
620    public function getFileList( array $params ) {
621        if ( isset( $params['forWrite'] ) && $params['forWrite'] ) {
622            return $this->getFileListForWrite( $params );
623        }
624
625        $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
626        return $this->backends[$this->masterIndex]->getFileList( $realParams );
627    }
628
629    private function getFileListForWrite( array $params ): array {
630        $files = [];
631        // Get the list of thumbnails from all backends to allow
632        // deleting all of them. Otherwise, old thumbnails existing on
633        // one backend only won't get updated in reupload (T331138).
634        foreach ( $this->backends as $backend ) {
635            $realParams = $this->substOpPaths( $params, $backend );
636            $iterator = $backend->getFileList( $realParams );
637            if ( $iterator !== null ) {
638                foreach ( $iterator as $file ) {
639                    $files[] = $file;
640                }
641            }
642        }
643
644        return array_unique( $files );
645    }
646
647    /** @inheritDoc */
648    public function getFeatures() {
649        return $this->backends[$this->masterIndex]->getFeatures();
650    }
651
652    /** @inheritDoc */
653    public function clearCache( ?array $paths = null ) {
654        foreach ( $this->backends as $backend ) {
655            $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
656            $backend->clearCache( $realPaths );
657        }
658    }
659
660    /** @inheritDoc */
661    public function preloadCache( array $paths ) {
662        $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] );
663        $this->backends[$this->readIndex]->preloadCache( $realPaths );
664    }
665
666    /** @inheritDoc */
667    public function preloadFileStat( array $params ) {
668        $index = $this->getReadIndexFromParams( $params );
669        $realParams = $this->substOpPaths( $params, $this->backends[$index] );
670
671        return $this->backends[$index]->preloadFileStat( $realParams );
672    }
673
674    /** @inheritDoc */
675    public function getScopedLocksForOps( array $ops, StatusValue $status ) {
676        $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
677        $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
678        // Get the paths to lock from the master backend
679        $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
680        // Get the paths under the proxy backend's name
681        $pbPaths = [
682            LockManager::LOCK_UW => $this->unsubstPaths(
683                $paths[LockManager::LOCK_UW],
684                $this->backends[$this->masterIndex]
685            ),
686            LockManager::LOCK_EX => $this->unsubstPaths(
687                $paths[LockManager::LOCK_EX],
688                $this->backends[$this->masterIndex]
689            )
690        ];
691
692        // Actually acquire the locks
693        return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );
694    }
695
696    /**
697     * @param array $params
698     * @return int The master or read affinity backend index, based on $params['latest']
699     */
700    protected function getReadIndexFromParams( array $params ) {
701        return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex;
702    }
703}
704
705/** @deprecated class alias since 1.43 */
706class_alias( FileBackendMultiWrite::class, 'FileBackendMultiWrite' );