wikimedia/mediawiki-core

View on GitHub
includes/libs/filebackend/MemoryFileBackend.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
/**
 * Simulation of a backend storage in memory.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup FileBackend
 */

use Wikimedia\AtEase\AtEase;
use Wikimedia\FileBackend\FileBackend;
use Wikimedia\Timestamp\ConvertibleTimestamp;

/**
 * Simulation of a backend storage in memory.
 *
 * All data in the backend is automatically deleted at the end of PHP execution.
 * Since the data stored here is volatile, this is only useful for staging or testing.
 *
 * @ingroup FileBackend
 * @since 1.23
 */
class MemoryFileBackend extends FileBackendStore {
    /** @var array Map of (file path => (data,mtime) */
    protected $files = [];

    public function getFeatures() {
        return self::ATTR_UNICODE_PATHS;
    }

    public function isPathUsableInternal( $storagePath ) {
        return ( $this->resolveHashKey( $storagePath ) !== null );
    }

    protected function doCreateInternal( array $params ) {
        $status = $this->newStatus();

        $dst = $this->resolveHashKey( $params['dst'] );
        if ( $dst === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );

            return $status;
        }

        $this->files[$dst] = [
            'data' => $params['content'],
            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
        ];

        return $status;
    }

    protected function doStoreInternal( array $params ) {
        $status = $this->newStatus();

        $dst = $this->resolveHashKey( $params['dst'] );
        if ( $dst === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );

            return $status;
        }

        AtEase::suppressWarnings();
        $data = file_get_contents( $params['src'] );
        AtEase::restoreWarnings();
        if ( $data === false ) { // source doesn't exist?
            $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );

            return $status;
        }

        $this->files[$dst] = [
            'data' => $data,
            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
        ];

        return $status;
    }

    protected function doCopyInternal( array $params ) {
        $status = $this->newStatus();

        $src = $this->resolveHashKey( $params['src'] );
        if ( $src === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['src'] );

            return $status;
        }

        $dst = $this->resolveHashKey( $params['dst'] );
        if ( $dst === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );

            return $status;
        }

        if ( !isset( $this->files[$src] ) ) {
            if ( empty( $params['ignoreMissingSource'] ) ) {
                $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
            }

            return $status;
        }

        $this->files[$dst] = [
            'data' => $this->files[$src]['data'],
            'mtime' => ConvertibleTimestamp::convert( TS_MW, time() )
        ];

        return $status;
    }

    protected function doMoveInternal( array $params ) {
        $status = $this->newStatus();

        $src = $this->resolveHashKey( $params['src'] );
        if ( $src === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['src'] );

            return $status;
        }

        $dst = $this->resolveHashKey( $params['dst'] );
        if ( $dst === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['dst'] );

            return $status;
        }

        if ( !isset( $this->files[$src] ) ) {
            if ( empty( $params['ignoreMissingSource'] ) ) {
                $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
            }

            return $status;
        }

        $this->files[$dst] = $this->files[$src];
        unset( $this->files[$src] );
        $this->files[$dst]['mtime'] = ConvertibleTimestamp::convert( TS_MW, time() );

        return $status;
    }

    protected function doDeleteInternal( array $params ) {
        $status = $this->newStatus();

        $src = $this->resolveHashKey( $params['src'] );
        if ( $src === null ) {
            $status->fatal( 'backend-fail-invalidpath', $params['src'] );

            return $status;
        }

        if ( !isset( $this->files[$src] ) ) {
            if ( empty( $params['ignoreMissingSource'] ) ) {
                $status->fatal( 'backend-fail-delete', $params['src'] );
            }

            return $status;
        }

        unset( $this->files[$src] );

        return $status;
    }

    protected function doGetFileStat( array $params ) {
        $src = $this->resolveHashKey( $params['src'] );
        if ( $src === null ) {
            return self::RES_ERROR; // invalid path
        }

        if ( isset( $this->files[$src] ) ) {
            return [
                'mtime' => $this->files[$src]['mtime'],
                'size' => strlen( $this->files[$src]['data'] ),
            ];
        }

        return self::RES_ABSENT;
    }

    protected function doGetLocalCopyMulti( array $params ) {
        $tmpFiles = []; // (path => TempFSFile)
        foreach ( $params['srcs'] as $srcPath ) {
            $src = $this->resolveHashKey( $srcPath );
            if ( $src === null ) {
                $fsFile = self::RES_ERROR;
            } elseif ( !isset( $this->files[$src] ) ) {
                $fsFile = self::RES_ABSENT;
            } else {
                // Create a new temporary file with the same extension...
                $ext = FileBackend::extensionFromPath( $src );
                $fsFile = $this->tmpFileFactory->newTempFSFile( 'localcopy_', $ext );
                if ( $fsFile ) {
                    $bytes = file_put_contents( $fsFile->getPath(), $this->files[$src]['data'] );
                    if ( $bytes !== strlen( $this->files[$src]['data'] ) ) {
                        $fsFile = self::RES_ERROR;
                    }
                }
            }
            $tmpFiles[$srcPath] = $fsFile;
        }

        return $tmpFiles;
    }

    protected function doDirectoryExists( $container, $dir, array $params ) {
        $prefix = rtrim( "$container/$dir", '/' ) . '/';
        foreach ( $this->files as $path => $data ) {
            if ( strpos( $path, $prefix ) === 0 ) {
                return true;
            }
        }

        return false;
    }

    public function getDirectoryListInternal( $container, $dir, array $params ) {
        $dirs = [];
        $prefix = rtrim( "$container/$dir", '/' ) . '/';
        $prefixLen = strlen( $prefix );
        foreach ( $this->files as $path => $data ) {
            if ( strpos( $path, $prefix ) === 0 ) {
                $relPath = substr( $path, $prefixLen );
                if ( $relPath === false ) {
                    continue;
                } elseif ( strpos( $relPath, '/' ) === false ) {
                    continue; // just a file
                }
                $parts = array_slice( explode( '/', $relPath ), 0, -1 ); // last part is file name
                if ( !empty( $params['topOnly'] ) ) {
                    $dirs[$parts[0]] = 1; // top directory
                } else {
                    $current = '';
                    foreach ( $parts as $part ) { // all directories
                        $dir = ( $current === '' ) ? $part : "$current/$part";
                        $dirs[$dir] = 1;
                        $current = $dir;
                    }
                }
            }
        }

        return array_keys( $dirs );
    }

    public function getFileListInternal( $container, $dir, array $params ) {
        $files = [];
        $prefix = rtrim( "$container/$dir", '/' ) . '/';
        $prefixLen = strlen( $prefix );
        foreach ( $this->files as $path => $data ) {
            if ( strpos( $path, $prefix ) === 0 ) {
                $relPath = substr( $path, $prefixLen );
                if ( $relPath === false ) {
                    continue;
                } elseif ( !empty( $params['topOnly'] ) && strpos( $relPath, '/' ) !== false ) {
                    continue;
                }
                $files[] = $relPath;
            }
        }

        return $files;
    }

    protected function directoriesAreVirtual() {
        return true;
    }

    /**
     * Get the absolute file system path for a storage path
     *
     * @param string $storagePath
     * @return string|null
     */
    protected function resolveHashKey( $storagePath ) {
        [ $fullCont, $relPath ] = $this->resolveStoragePathReal( $storagePath );
        if ( $relPath === null ) {
            return null; // invalid
        }

        return ( $relPath !== '' ) ? "$fullCont/$relPath" : $fullCont;
    }
}