Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.89% covered (warning)
86.89%
106 / 122
66.67% covered (warning)
66.67%
10 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
MemoryFileBackend
87.60% covered (warning)
87.60%
106 / 121
66.67% covered (warning)
66.67%
10 / 15
52.39
0.00% covered (danger)
0.00%
0 / 1
 getFeatures
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isPathUsableInternal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doCreateInternal
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
2.03
 doStoreInternal
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
3.21
 doCopyInternal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 doMoveInternal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copyInMemory
75.00% covered (warning)
75.00%
15 / 20
0.00% covered (danger)
0.00%
0 / 1
6.56
 doDeleteInternal
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
4.32
 doGetFileStat
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 doGetLocalCopyMulti
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
6.01
 doDirectoryExists
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getDirectoryListInternal
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 getFileListInternal
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 directoriesAreVirtual
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveHashKey
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Simulation of a backend storage in memory.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup FileBackend
8 */
9
10namespace Wikimedia\FileBackend;
11
12use Wikimedia\Timestamp\ConvertibleTimestamp;
13use Wikimedia\Timestamp\TimestampFormat as TS;
14
15/**
16 * Simulation of a backend storage in memory.
17 *
18 * All data in the backend is automatically deleted at the end of PHP execution.
19 * Since the data stored here is volatile, this is only useful for staging or testing.
20 *
21 * @ingroup FileBackend
22 * @since 1.23
23 */
24class MemoryFileBackend extends FileBackendStore {
25    /** @var array Map of (file path => (data,mtime) */
26    protected $files = [];
27
28    /** @inheritDoc */
29    public function getFeatures() {
30        return self::ATTR_UNICODE_PATHS;
31    }
32
33    /** @inheritDoc */
34    public function isPathUsableInternal( $storagePath ) {
35        return ( $this->resolveHashKey( $storagePath ) !== null );
36    }
37
38    /** @inheritDoc */
39    protected function doCreateInternal( array $params ) {
40        $status = $this->newStatus();
41
42        $dst = $this->resolveHashKey( $params['dst'] );
43        if ( $dst === null ) {
44            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
45
46            return $status;
47        }
48
49        $this->files[$dst] = [
50            'data' => $params['content'],
51            'mtime' => ConvertibleTimestamp::convert( TS::MW, time() )
52        ];
53
54        return $status;
55    }
56
57    /** @inheritDoc */
58    protected function doStoreInternal( array $params ) {
59        $status = $this->newStatus();
60
61        $dst = $this->resolveHashKey( $params['dst'] );
62        if ( $dst === null ) {
63            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
64
65            return $status;
66        }
67
68        // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
69        $data = @file_get_contents( $params['src'] );
70        if ( $data === false ) { // source doesn't exist?
71            $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
72
73            return $status;
74        }
75
76        $this->files[$dst] = [
77            'data' => $data,
78            'mtime' => ConvertibleTimestamp::convert( TS::MW, time() )
79        ];
80
81        return $status;
82    }
83
84    /** @inheritDoc */
85    protected function doCopyInternal( array $params ) {
86        return $this->copyInMemory( $params, 'copy' );
87    }
88
89    /** @inheritDoc */
90    protected function doMoveInternal( array $params ) {
91        return $this->copyInMemory( $params, 'move' );
92    }
93
94    /**
95     * @param array $params
96     * @param string $action whether it's 'copy' or 'move'
97     * @return \StatusValue
98     */
99    private function copyInMemory( array $params, string $action ) {
100        $status = $this->newStatus();
101
102        $src = $this->resolveHashKey( $params['src'] );
103        if ( $src === null ) {
104            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
105
106            return $status;
107        }
108
109        $dst = $this->resolveHashKey( $params['dst'] );
110        if ( $dst === null ) {
111            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
112
113            return $status;
114        }
115
116        if ( !isset( $this->files[$src] ) ) {
117            if ( empty( $params['ignoreMissingSource'] ) ) {
118                // Error codes: backend-fail-copy, backend-fail-move
119                $status->fatal( 'backend-fail-' . $action, $params['src'], $params['dst'] );
120            }
121
122            return $status;
123        }
124
125        $this->files[$dst] = [
126            'data' => $this->files[$src]['data'],
127            'mtime' => ConvertibleTimestamp::convert( TS::MW, time() )
128        ];
129
130        if ( $action === 'move' ) {
131            unset( $this->files[$src] );
132        }
133        return $status;
134    }
135
136    /** @inheritDoc */
137    protected function doDeleteInternal( array $params ) {
138        $status = $this->newStatus();
139
140        $src = $this->resolveHashKey( $params['src'] );
141        if ( $src === null ) {
142            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
143
144            return $status;
145        }
146
147        if ( !isset( $this->files[$src] ) ) {
148            if ( empty( $params['ignoreMissingSource'] ) ) {
149                $status->fatal( 'backend-fail-delete', $params['src'] );
150            }
151
152            return $status;
153        }
154
155        unset( $this->files[$src] );
156
157        return $status;
158    }
159
160    /** @inheritDoc */
161    protected function doGetFileStat( array $params ) {
162        $src = $this->resolveHashKey( $params['src'] );
163        if ( $src === null ) {
164            return self::RES_ERROR; // invalid path
165        }
166
167        if ( isset( $this->files[$src] ) ) {
168            return [
169                'mtime' => $this->files[$src]['mtime'],
170                'size' => strlen( $this->files[$src]['data'] ),
171            ];
172        }
173
174        return self::RES_ABSENT;
175    }
176
177    /** @inheritDoc */
178    protected function doGetLocalCopyMulti( array $params ) {
179        $tmpFiles = []; // (path => TempFSFile)
180        foreach ( $params['srcs'] as $srcPath ) {
181            $src = $this->resolveHashKey( $srcPath );
182            if ( $src === null ) {
183                $fsFile = self::RES_ERROR;
184            } elseif ( !isset( $this->files[$src] ) ) {
185                $fsFile = self::RES_ABSENT;
186            } else {
187                // Create a new temporary file with the same extension...
188                $ext = FileBackend::extensionFromPath( $src );
189                $fsFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
190                if ( $fsFile ) {
191                    $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
192                    if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
193                        $fsFile = self::RES_ERROR;
194                    }
195                }
196            }
197            $tmpFiles[$srcPath] = $fsFile;
198        }
199
200        return $tmpFiles;
201    }
202
203    /** @inheritDoc */
204    protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
205        $prefix = rtrim( "$fullCont/$dirRel", '/' ) . '/';
206        foreach ( $this->files as $path => $data ) {
207            if ( str_starts_with( $path, $prefix ) ) {
208                return true;
209            }
210        }
211
212        return false;
213    }
214
215    /** @inheritDoc */
216    public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
217        $dirs = [];
218        $prefix = rtrim( "$fullCont/$dirRel", '/' ) . '/';
219        $prefixLen = strlen( $prefix );
220        foreach ( $this->files as $path => $data ) {
221            if ( str_starts_with( $path, $prefix ) ) {
222                $relPath = substr( $path, $prefixLen );
223                if ( !str_contains( $relPath, '/' ) ) {
224                    continue; // just a file
225                }
226                $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
227                if ( !empty( $params['topOnly'] ) ) {
228                    $dirs[$parts[0]] = 1; // top directory
229                } else {
230                    $current = '';
231                    foreach ( $parts as $part ) { // all directories
232                        $dirRel = ( $current === '' ) ? $part : "$current/$part";
233                        $dirs[$dirRel] = 1;
234                        $current = $dirRel;
235                    }
236                }
237            }
238        }
239
240        return array_keys( $dirs );
241    }
242
243    /** @inheritDoc */
244    public function getFileListInternal( $fullCont, $dirRel, array $params ) {
245        $files = [];
246        $prefix = rtrim( "$fullCont/$dirRel", '/' ) . '/';
247        $prefixLen = strlen( $prefix );
248        foreach ( $this->files as $path => $data ) {
249            if ( str_starts_with( $path, $prefix ) ) {
250                $relPath = substr( $path, $prefixLen );
251                if (
252                    $relPath === '' ||
253                    ( !empty( $params['topOnly'] ) && str_contains( $relPath, '/' ) )
254                ) {
255                    continue;
256                }
257                $files[] = $relPath;
258            }
259        }
260
261        return $files;
262    }
263
264    /** @inheritDoc */
265    protected function directoriesAreVirtual() {
266        return true;
267    }
268
269    /**
270     * Get the absolute file system path for a storage path
271     *
272     * @param string $storagePath
273     * @return string|null
274     */
275    protected function resolveHashKey( $storagePath ) {
276        [ $fullCont, $relPath ] = $this->resolveStoragePathReal( $storagePath );
277        if ( $relPath === null ) {
278            return null; // invalid
279        }
280
281        return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
282    }
283}
284
285/** @deprecated class alias since 1.43 */
286class_alias( MemoryFileBackend::class, 'MemoryFileBackend' );