Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.17% covered (warning)
84.17%
117 / 139
42.86% covered (danger)
42.86%
6 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
MemoryFileBackend
84.17% covered (warning)
84.17%
117 / 139
42.86% covered (danger)
42.86%
6 / 14
61.31
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
75.00% covered (warning)
75.00%
12 / 16
0.00% covered (danger)
0.00%
0 / 1
3.14
 doCopyInternal
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
5.54
 doMoveInternal
70.59% covered (warning)
70.59%
12 / 17
0.00% covered (danger)
0.00%
0 / 1
5.64
 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
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
8.01
 getFileListInternal
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 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 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup FileBackend
22 */
23
24use Wikimedia\AtEase\AtEase;
25use Wikimedia\Timestamp\ConvertibleTimestamp;
26
27/**
28 * Simulation of a backend storage in memory.
29 *
30 * All data in the backend is automatically deleted at the end of PHP execution.
31 * Since the data stored here is volatile, this is only useful for staging or testing.
32 *
33 * @ingroup FileBackend
34 * @since 1.23
35 */
36class MemoryFileBackend extends FileBackendStore {
37    /** @var array Map of (file path => (data,mtime) */
38    protected $files = [];
39
40    public function getFeatures() {
41        return self::ATTR_UNICODE_PATHS;
42    }
43
44    public function isPathUsableInternal( $storagePath ) {
45        return ( $this->resolveHashKey( $storagePath ) !== null );
46    }
47
48    protected function doCreateInternal( array $params ) {
49        $status = $this->newStatus();
50
51        $dst = $this->resolveHashKey( $params['dst'] );
52        if ( $dst === null ) {
53            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
54
55            return $status;
56        }
57
58        $this->files[$dst] = [
59            'data' => $params['content'],
60            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
61        ];
62
63        return $status;
64    }
65
66    protected function doStoreInternal( array $params ) {
67        $status = $this->newStatus();
68
69        $dst = $this->resolveHashKey( $params['dst'] );
70        if ( $dst === null ) {
71            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
72
73            return $status;
74        }
75
76        AtEase::suppressWarnings();
77        $data = file_get_contents( $params['src'] );
78        AtEase::restoreWarnings();
79        if ( $data === false ) { // source doesn't exist?
80            $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
81
82            return $status;
83        }
84
85        $this->files[$dst] = [
86            'data' => $data,
87            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
88        ];
89
90        return $status;
91    }
92
93    protected function doCopyInternal( array $params ) {
94        $status = $this->newStatus();
95
96        $src = $this->resolveHashKey( $params['src'] );
97        if ( $src === null ) {
98            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
99
100            return $status;
101        }
102
103        $dst = $this->resolveHashKey( $params['dst'] );
104        if ( $dst === null ) {
105            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
106
107            return $status;
108        }
109
110        if ( !isset( $this->files[$src] ) ) {
111            if ( empty( $params['ignoreMissingSource'] ) ) {
112                $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
113            }
114
115            return $status;
116        }
117
118        $this->files[$dst] = [
119            'data' => $this->files[$src]['data'],
120            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
121        ];
122
123        return $status;
124    }
125
126    protected function doMoveInternal( array $params ) {
127        $status = $this->newStatus();
128
129        $src = $this->resolveHashKey( $params['src'] );
130        if ( $src === null ) {
131            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
132
133            return $status;
134        }
135
136        $dst = $this->resolveHashKey( $params['dst'] );
137        if ( $dst === null ) {
138            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
139
140            return $status;
141        }
142
143        if ( !isset( $this->files[$src] ) ) {
144            if ( empty( $params['ignoreMissingSource'] ) ) {
145                $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
146            }
147
148            return $status;
149        }
150
151        $this->files[$dst] = $this->files[$src];
152        unset( $this->files[$src] );
153        $this->files[$dst]['mtime'] = ConvertibleTimestamp::convert( TS_MW, time() );
154
155        return $status;
156    }
157
158    protected function doDeleteInternal( array $params ) {
159        $status = $this->newStatus();
160
161        $src = $this->resolveHashKey( $params['src'] );
162        if ( $src === null ) {
163            $status->fatal( 'backend-fail-invalidpath', $params['src'] );
164
165            return $status;
166        }
167
168        if ( !isset( $this->files[$src] ) ) {
169            if ( empty( $params['ignoreMissingSource'] ) ) {
170                $status->fatal( 'backend-fail-delete', $params['src'] );
171            }
172
173            return $status;
174        }
175
176        unset( $this->files[$src] );
177
178        return $status;
179    }
180
181    protected function doGetFileStat( array $params ) {
182        $src = $this->resolveHashKey( $params['src'] );
183        if ( $src === null ) {
184            return self::RES_ERROR; // invalid path
185        }
186
187        if ( isset( $this->files[$src] ) ) {
188            return [
189                'mtime' => $this->files[$src]['mtime'],
190                'size' => strlen( $this->files[$src]['data'] ),
191            ];
192        }
193
194        return self::RES_ABSENT;
195    }
196
197    protected function doGetLocalCopyMulti( array $params ) {
198        $tmpFiles = []; // (path => TempFSFile)
199        foreach ( $params['srcs'] as $srcPath ) {
200            $src = $this->resolveHashKey( $srcPath );
201            if ( $src === null ) {
202                $fsFile = self::RES_ERROR;
203            } elseif ( !isset( $this->files[$src] ) ) {
204                $fsFile = self::RES_ABSENT;
205            } else {
206                // Create a new temporary file with the same extension...
207                $ext = FileBackend::extensionFromPath( $src );
208                $fsFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
209                if ( $fsFile ) {
210                    $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
211                    if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
212                        $fsFile = self::RES_ERROR;
213                    }
214                }
215            }
216            $tmpFiles[$srcPath] = $fsFile;
217        }
218
219        return $tmpFiles;
220    }
221
222    protected function doDirectoryExists( $container, $dir, array $params ) {
223        $prefix = rtrim( "$container/$dir", '/' ) . '/';
224        foreach ( $this->files as $path => $data ) {
225            if ( strpos( $path, $prefix ) === 0 ) {
226                return true;
227            }
228        }
229
230        return false;
231    }
232
233    public function getDirectoryListInternal( $container, $dir, array $params ) {
234        $dirs = [];
235        $prefix = rtrim( "$container/$dir", '/' ) . '/';
236        $prefixLen = strlen( $prefix );
237        foreach ( $this->files as $path => $data ) {
238            if ( strpos( $path, $prefix ) === 0 ) {
239                $relPath = substr( $path, $prefixLen );
240                if ( $relPath === false ) {
241                    continue;
242                } elseif ( strpos( $relPath, '/' ) === false ) {
243                    continue; // just a file
244                }
245                $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
246                if ( !empty( $params['topOnly'] ) ) {
247                    $dirs[$parts[0]] = 1; // top directory
248                } else {
249                    $current = '';
250                    foreach ( $parts as $part ) { // all directories
251                        $dir = ( $current === '' ) ? $part : "$current/$part";
252                        $dirs[$dir] = 1;
253                        $current = $dir;
254                    }
255                }
256            }
257        }
258
259        return array_keys( $dirs );
260    }
261
262    public function getFileListInternal( $container, $dir, array $params ) {
263        $files = [];
264        $prefix = rtrim( "$container/$dir", '/' ) . '/';
265        $prefixLen = strlen( $prefix );
266        foreach ( $this->files as $path => $data ) {
267            if ( strpos( $path, $prefix ) === 0 ) {
268                $relPath = substr( $path, $prefixLen );
269                if ( $relPath === false ) {
270                    continue;
271                } elseif ( !empty( $params['topOnly'] ) && strpos( $relPath, '/' ) !== false ) {
272                    continue;
273                }
274                $files[] = $relPath;
275            }
276        }
277
278        return $files;
279    }
280
281    protected function directoriesAreVirtual() {
282        return true;
283    }
284
285    /**
286     * Get the absolute file system path for a storage path
287     *
288     * @param string $storagePath
289     * @return string|null
290     */
291    protected function resolveHashKey( $storagePath ) {
292        [ $fullCont, $relPath ] = $this->resolveStoragePathReal( $storagePath );
293        if ( $relPath === null ) {
294            return null; // invalid
295        }
296
297        return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
298    }
299}