Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.57% covered (warning)
59.57%
277 / 465
29.27% covered (danger)
29.27%
12 / 41
CRAP
0.00% covered (danger)
0.00%
0 / 1
FSFileBackend
59.57% covered (warning)
59.57%
277 / 465
29.27% covered (danger)
29.27%
12 / 41
2707.95
0.00% covered (danger)
0.00%
0 / 1
 __construct
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
9.37
 getFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveContainerPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isLegalRelPath
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
3.58
 containerFSRoot
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 resolveToFSPath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 isPathUsableInternal
57.14% covered (warning)
57.14%
8 / 14
0.00% covered (danger)
0.00%
0 / 1
10.86
 doCreateInternal
51.52% covered (warning)
51.52%
17 / 33
0.00% covered (danger)
0.00%
0 / 1
24.79
 doStoreInternal
62.86% covered (warning)
62.86%
22 / 35
0.00% covered (danger)
0.00%
0 / 1
21.66
 doCopyInternal
64.10% covered (warning)
64.10%
25 / 39
0.00% covered (danger)
0.00%
0 / 1
27.84
 doMoveInternal
48.15% covered (danger)
48.15%
13 / 27
0.00% covered (danger)
0.00%
0 / 1
27.87
 doDeleteInternal
47.62% covered (danger)
47.62%
10 / 21
0.00% covered (danger)
0.00%
0 / 1
20.64
 doPrepareInternal
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
10.90
 doSecureInternal
38.89% covered (danger)
38.89%
7 / 18
0.00% covered (danger)
0.00%
0 / 1
22.61
 doPublishInternal
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
22.50
 doCleanInternal
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 doGetFileStat
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 doClearCache
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 doDirectoryExists
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getDirectoryListInternal
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
5.93
 getFileListInternal
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
6.75
 doGetLocalReferenceMulti
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
5.01
 doGetLocalCopyMulti
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
7.50
 directoriesAreVirtual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doExecuteOpHandlesInternal
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
6.00
 makeStagingPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeCopyCommand
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 makeMoveCommand
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 makeUnlinkCommand
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 chmod
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 unlink
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 rmdir
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 newTempFileWithContent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 indexHtmlPrivate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 htaccessPrivate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cleanPathSlashes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 trapWarnings
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 trapWarningsIgnoringNotFound
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 untrapWarnings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getFileNotFoundRegex
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
19.85
 isFileNotFoundError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup FileBackend
20 */
21
22/**
23 * File system based backend.
24 *
25 * This program is free software; you can redistribute it and/or modify
26 * it under the terms of the GNU General Public License as published by
27 * the Free Software Foundation; either version 2 of the License, or
28 * (at your option) any later version.
29 *
30 * This program is distributed in the hope that it will be useful,
31 * but WITHOUT ANY WARRANTY; without even the implied warranty of
32 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 * GNU General Public License for more details.
34 *
35 * You should have received a copy of the GNU General Public License along
36 * with this program; if not, write to the Free Software Foundation, Inc.,
37 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
38 * http://www.gnu.org/copyleft/gpl.html
39 *
40 * @file
41 * @ingroup FileBackend
42 */
43
44use Shellbox\Shellbox;
45use Wikimedia\AtEase\AtEase;
46use Wikimedia\FileBackend\FileBackend;
47use Wikimedia\Timestamp\ConvertibleTimestamp;
48
49/**
50 * @brief Class for a file system (FS) based file backend.
51 *
52 * All "containers" each map to a directory under the backend's base directory.
53 * For backwards-compatibility, some container paths can be set to custom paths.
54 * The domain ID will not be used in any custom paths, so this should be avoided.
55 *
56 * Having directories with thousands of files will diminish performance.
57 * Sharding can be accomplished by using FileRepo-style hash paths.
58 *
59 * StatusValue messages should avoid mentioning the internal FS paths.
60 * PHP warnings are assumed to be logged rather than output.
61 *
62 * @ingroup FileBackend
63 * @since 1.19
64 */
65class FSFileBackend extends FileBackendStore {
66    /** @var MapCacheLRU Cache for known prepared/usable directories */
67    protected $usableDirCache;
68
69    /** @var string|null Directory holding the container directories */
70    protected $basePath;
71
72    /** @var array<string,string> Map of container names to root paths for custom container paths */
73    protected $containerPaths;
74
75    /** @var int Directory permission mode */
76    protected $dirMode;
77    /** @var int File permission mode */
78    protected $fileMode;
79    /** @var string Required OS username to own files */
80    protected $fileOwner;
81
82    /** @var string Simpler version of PHP_OS_FAMILY */
83    protected $os;
84    /** @var string OS username running this script */
85    protected $currentUser;
86
87    /** @var bool[] Map of (stack index => whether a warning happened) */
88    private $warningTrapStack = [];
89
90    /**
91     * @see FileBackendStore::__construct()
92     * Additional $config params include:
93     *   - basePath       : File system directory that holds containers.
94     *   - containerPaths : Map of container names to custom file system directories.
95     *                      This should only be used for backwards-compatibility.
96     *   - fileMode       : Octal UNIX file permissions to use on files stored.
97     *   - directoryMode  : Octal UNIX file permissions to use on directories created.
98     * @param array $config
99     */
100    public function __construct( array $config ) {
101        parent::__construct( $config );
102
103        if ( PHP_OS_FAMILY === 'Windows' ) {
104            $this->os = 'Windows';
105        } elseif ( PHP_OS_FAMILY === 'BSD' || PHP_OS_FAMILY === 'Darwin' ) {
106            $this->os = 'BSD';
107        } else {
108            $this->os = 'Linux';
109        }
110        // Remove any possible trailing slash from directories
111        if ( isset( $config['basePath'] ) ) {
112            $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
113        } else {
114            $this->basePath = null; // none; containers must have explicit paths
115        }
116
117        $this->containerPaths = [];
118        foreach ( ( $config['containerPaths'] ?? [] ) as $container => $fsPath ) {
119            $this->containerPaths[$container] = rtrim( $fsPath, '/' ); // remove trailing slash
120        }
121
122        $this->fileMode = $config['fileMode'] ?? 0644;
123        $this->dirMode = $config['directoryMode'] ?? 0777;
124        if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
125            $this->fileOwner = $config['fileOwner'];
126            // Cache this, assuming it doesn't change
127            $this->currentUser = posix_getpwuid( posix_getuid() )['name'];
128        }
129
130        $this->usableDirCache = new MapCacheLRU( self::CACHE_CHEAP_SIZE );
131    }
132
133    public function getFeatures() {
134        return self::ATTR_UNICODE_PATHS;
135    }
136
137    protected function resolveContainerPath( $container, $relStoragePath ) {
138        // Check that container has a root directory
139        if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
140            // Check for sensible relative paths (assume the base paths are OK)
141            if ( $this->isLegalRelPath( $relStoragePath ) ) {
142                return $relStoragePath;
143            }
144        }
145
146        return null; // invalid
147    }
148
149    /**
150     * Check a relative file system path for validity
151     *
152     * @param string $fsPath Normalized relative path
153     * @return bool
154     */
155    protected function isLegalRelPath( $fsPath ) {
156        // Check for file names longer than 255 chars
157        if ( preg_match( '![^/]{256}!', $fsPath ) ) { // ext3/NTFS
158            return false;
159        }
160        if ( $this->os === 'Windows' ) { // NTFS
161            return !preg_match( '![:*?"<>|]!', $fsPath );
162        } else {
163            return true;
164        }
165    }
166
167    /**
168     * Given the short (unresolved) and full (resolved) name of
169     * a container, return the file system path of the container.
170     *
171     * @param string $shortCont
172     * @param string $fullCont
173     * @return string|null
174     */
175    protected function containerFSRoot( $shortCont, $fullCont ) {
176        if ( isset( $this->containerPaths[$shortCont] ) ) {
177            return $this->containerPaths[$shortCont];
178        } elseif ( isset( $this->basePath ) ) {
179            return "{$this->basePath}/{$fullCont}";
180        }
181
182        return null; // no container base path defined
183    }
184
185    /**
186     * Get the absolute file system path for a storage path
187     *
188     * @param string $storagePath
189     * @return string|null
190     */
191    protected function resolveToFSPath( $storagePath ) {
192        [ $fullCont, $relPath ] = $this->resolveStoragePathReal( $storagePath );
193        if ( $relPath === null ) {
194            return null; // invalid
195        }
196        [ , $shortCont, ] = FileBackend::splitStoragePath( $storagePath );
197        $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
198        if ( $relPath != '' ) {
199            $fsPath .= "/{$relPath}";
200        }
201
202        return $fsPath;
203    }
204
205    public function isPathUsableInternal( $storagePath ) {
206        $fsPath = $this->resolveToFSPath( $storagePath );
207        if ( $fsPath === null ) {
208            return false; // invalid
209        }
210
211        if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
212            trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
213            return false;
214        }
215
216        $fsDirectory = dirname( $fsPath );
217        $usable = $this->usableDirCache->get( $fsDirectory, MapCacheLRU::TTL_PROC_SHORT );
218        if ( $usable === null ) {
219            AtEase::suppressWarnings();
220            $usable = is_dir( $fsDirectory ) && is_writable( $fsDirectory );
221            AtEase::restoreWarnings();
222            $this->usableDirCache->set( $fsDirectory, $usable ? 1 : 0 );
223        }
224
225        return $usable;
226    }
227
228    protected function doCreateInternal( array $params ) {
229        $status = $this->newStatus();
230
231        $fsDstPath = $this->resolveToFSPath( $params['dst'] );
232        if ( $fsDstPath === null ) {
233            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
234
235            return $status;
236        }
237
238        if ( !empty( $params['async'] ) ) { // deferred
239            $tempFile = $this->newTempFileWithContent( $params );
240            if ( !$tempFile ) {
241                $status->fatal( 'backend-fail-create', $params['dst'] );
242
243                return $status;
244            }
245            $cmd = $this->makeCopyCommand( $tempFile->getPath(), $fsDstPath, false );
246            $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
247                if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
248                    $status->fatal( 'backend-fail-create', $params['dst'] );
249                    trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
250                }
251            };
252            $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
253            $tempFile->bind( $status->value );
254        } else { // immediate write
255            $created = false;
256            // Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
257            // inode are unaffected since it writes to a new inode, and (c) new threads reading
258            // the file will either totally see the old version or totally see the new version
259            $fsStagePath = $this->makeStagingPath( $fsDstPath );
260            $this->trapWarningsIgnoringNotFound();
261            $stageHandle = fopen( $fsStagePath, 'xb' );
262            if ( $stageHandle ) {
263                $bytes = fwrite( $stageHandle, $params['content'] );
264                $created = ( $bytes === strlen( $params['content'] ) );
265                fclose( $stageHandle );
266                $created = $created ? rename( $fsStagePath, $fsDstPath ) : false;
267            }
268            $hadError = $this->untrapWarnings();
269            if ( $hadError || !$created ) {
270                $status->fatal( 'backend-fail-create', $params['dst'] );
271
272                return $status;
273            }
274            $this->chmod( $fsDstPath );
275        }
276
277        return $status;
278    }
279
280    protected function doStoreInternal( array $params ) {
281        $status = $this->newStatus();
282
283        $fsSrcPath = $params['src']; // file system path
284        $fsDstPath = $this->resolveToFSPath( $params['dst'] );
285        if ( $fsDstPath === null ) {
286            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
287
288            return $status;
289        }
290
291        if ( $fsSrcPath === $fsDstPath ) {
292            $status->fatal( 'backend-fail-internal', $this->name );
293
294            return $status;
295        }
296
297        if ( !empty( $params['async'] ) ) { // deferred
298            $cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath, false );
299            $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
300                if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
301                    $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
302                    trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
303                }
304            };
305            $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
306        } else { // immediate write
307            $stored = false;
308            // Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
309            // inode are unaffected since it writes to a new inode, and (c) new threads reading
310            // the file will either totally see the old version or totally see the new version
311            $fsStagePath = $this->makeStagingPath( $fsDstPath );
312            $this->trapWarningsIgnoringNotFound();
313            $srcHandle = fopen( $fsSrcPath, 'rb' );
314            if ( $srcHandle ) {
315                $stageHandle = fopen( $fsStagePath, 'xb' );
316                if ( $stageHandle ) {
317                    $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
318                    $stored = ( $bytes !== false && $bytes === fstat( $srcHandle )['size'] );
319                    fclose( $stageHandle );
320                    $stored = $stored ? rename( $fsStagePath, $fsDstPath ) : false;
321                }
322                fclose( $srcHandle );
323            }
324            $hadError = $this->untrapWarnings();
325            if ( $hadError || !$stored ) {
326                $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
327
328                return $status;
329            }
330            $this->chmod( $fsDstPath );
331        }
332
333        return $status;
334    }
335
336    protected function doCopyInternal( array $params ) {
337        $status = $this->newStatus();
338
339        $fsSrcPath = $this->resolveToFSPath( $params['src'] );
340        if ( $fsSrcPath === null ) {
341            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
342
343            return $status;
344        }
345
346        $fsDstPath = $this->resolveToFSPath( $params['dst'] );
347        if ( $fsDstPath === null ) {
348            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
349
350            return $status;
351        }
352
353        if ( $fsSrcPath === $fsDstPath ) {
354            return $status; // no-op
355        }
356
357        $ignoreMissing = !empty( $params['ignoreMissingSource'] );
358
359        if ( !empty( $params['async'] ) ) { // deferred
360            $cmd = $this->makeCopyCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
361            $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
362                if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
363                    $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
364                    trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
365                }
366            };
367            $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
368        } else { // immediate write
369            $copied = false;
370            // Use fwrite+rename since (a) this clears xattrs, (b) threads still reading the old
371            // inode are unaffected since it writes to a new inode, and (c) new threads reading
372            // the file will either totally see the old version or totally see the new version
373            $fsStagePath = $this->makeStagingPath( $fsDstPath );
374            $this->trapWarningsIgnoringNotFound();
375            $srcHandle = fopen( $fsSrcPath, 'rb' );
376            if ( $srcHandle ) {
377                $stageHandle = fopen( $fsStagePath, 'xb' );
378                if ( $stageHandle ) {
379                    $bytes = stream_copy_to_stream( $srcHandle, $stageHandle );
380                    $copied = ( $bytes !== false && $bytes === fstat( $srcHandle )['size'] );
381                    fclose( $stageHandle );
382                    $copied = $copied ? rename( $fsStagePath, $fsDstPath ) : false;
383                }
384                fclose( $srcHandle );
385            }
386            $hadError = $this->untrapWarnings();
387            if ( $hadError || ( !$copied && !$ignoreMissing ) ) {
388                $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
389
390                return $status;
391            }
392            if ( $copied ) {
393                $this->chmod( $fsDstPath );
394            }
395        }
396
397        return $status;
398    }
399
400    protected function doMoveInternal( array $params ) {
401        $status = $this->newStatus();
402
403        $fsSrcPath = $this->resolveToFSPath( $params['src'] );
404        if ( $fsSrcPath === null ) {
405            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
406
407            return $status;
408        }
409
410        $fsDstPath = $this->resolveToFSPath( $params['dst'] );
411        if ( $fsDstPath === null ) {
412            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
413
414            return $status;
415        }
416
417        if ( $fsSrcPath === $fsDstPath ) {
418            return $status; // no-op
419        }
420
421        $ignoreMissing = !empty( $params['ignoreMissingSource'] );
422
423        if ( !empty( $params['async'] ) ) { // deferred
424            $cmd = $this->makeMoveCommand( $fsSrcPath, $fsDstPath, $ignoreMissing );
425            $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
426                if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
427                    $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
428                    trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
429                }
430            };
431            $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
432        } else { // immediate write
433            // Use rename() here since (a) this clears xattrs, (b) any threads still reading the
434            // old inode are unaffected since it writes to a new inode, and (c) this is fast and
435            // atomic within a file system volume (as is normally the case)
436            $this->trapWarningsIgnoringNotFound();
437            $moved = rename( $fsSrcPath, $fsDstPath );
438            $hadError = $this->untrapWarnings();
439            if ( $hadError || ( !$moved && !$ignoreMissing ) ) {
440                $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
441
442                return $status;
443            }
444        }
445
446        return $status;
447    }
448
449    protected function doDeleteInternal( array $params ) {
450        $status = $this->newStatus();
451
452        $fsSrcPath = $this->resolveToFSPath( $params['src'] );
453        if ( $fsSrcPath === null ) {
454            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
455
456            return $status;
457        }
458
459        $ignoreMissing = !empty( $params['ignoreMissingSource'] );
460
461        if ( !empty( $params['async'] ) ) { // deferred
462            $cmd = $this->makeUnlinkCommand( $fsSrcPath, $ignoreMissing );
463            $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
464                if ( $errors !== '' && !( $this->os === 'Windows' && $errors[0] === " " ) ) {
465                    $status->fatal( 'backend-fail-delete', $params['src'] );
466                    trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
467                }
468            };
469            $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
470        } else { // immediate write
471            $this->trapWarningsIgnoringNotFound();
472            $deleted = unlink( $fsSrcPath );
473            $hadError = $this->untrapWarnings();
474            if ( $hadError || ( !$deleted && !$ignoreMissing ) ) {
475                $status->fatal( 'backend-fail-delete', $params['src'] );
476
477                return $status;
478            }
479        }
480
481        return $status;
482    }
483
484    /**
485     * @inheritDoc
486     */
487    protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
488        $status = $this->newStatus();
489        [ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
490        $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
491        $fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
492        // Create the directory and its parents as needed...
493        $created = false;
494        AtEase::suppressWarnings();
495        $alreadyExisted = is_dir( $fsDirectory ); // already there?
496        if ( !$alreadyExisted ) {
497            $created = mkdir( $fsDirectory, $this->dirMode, true );
498            if ( !$created ) {
499                $alreadyExisted = is_dir( $fsDirectory ); // another thread made it?
500            }
501        }
502        $isWritable = $created ?: is_writable( $fsDirectory ); // assume writable if created here
503        AtEase::restoreWarnings();
504        if ( !$alreadyExisted && !$created ) {
505            $this->logger->error( __METHOD__ . ": cannot create directory $fsDirectory" );
506            $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
507        } elseif ( !$isWritable ) {
508            $this->logger->error( __METHOD__ . ": directory $fsDirectory is read-only" );
509            $status->fatal( 'directoryreadonlyerror', $params['dir'] );
510        }
511        // Respect any 'noAccess' or 'noListing' flags...
512        if ( $created ) {
513            $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
514        }
515
516        if ( $status->isGood() ) {
517            $this->usableDirCache->set( $fsDirectory, 1 );
518        }
519
520        return $status;
521    }
522
523    protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
524        $status = $this->newStatus();
525        [ , $shortCont, ] = FileBackend::splitStoragePath( $params['dir'] );
526        $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
527        $fsDirectory = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
528        // Seed new directories with a blank index.html, to prevent crawling...
529        if ( !empty( $params['noListing'] ) && !is_file( "{$fsDirectory}/index.html" ) ) {
530            $this->trapWarnings();
531            $bytes = file_put_contents( "{$fsDirectory}/index.html", $this->indexHtmlPrivate() );
532            $this->untrapWarnings();
533            if ( $bytes === false ) {
534                $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
535            }
536        }
537        // Add a .htaccess file to the root of the container...
538        if ( !empty( $params['noAccess'] ) && !is_file( "{$contRoot}/.htaccess" ) ) {
539