Implement local file storage

This commit is contained in:
Alexander A. Klimov 2017-11-06 10:22:18 +01:00
parent 2caa07ce9b
commit 2cced5fe13
5 changed files with 553 additions and 0 deletions

View File

@ -0,0 +1,150 @@
<?php
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Storage;
use ErrorException;
use Icinga\Exception\AlreadyExistsException;
use Icinga\Exception\NotFoundError;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use InvalidArgumentException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use UnexpectedValueException;
/**
* Stores files in the local file system
*/
class LocalFileStorage implements StorageInterface
{
/**
* The root directory of this storage
*
* @var string
*/
protected $baseDir;
/**
* Constructor
*
* @param string $baseDir The root directory of this storage
*/
public function __construct($baseDir)
{
$this->baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR);
}
public function getIterator()
{
try {
return new LocalFileStorageIterator($this->baseDir);
} catch (UnexpectedValueException $e) {
throw new NotReadableError('Couldn\'t read the directory "%s": %s', $this->baseDir, $e);
}
}
public function has($path)
{
return is_file($this->resolvePath($path));
}
public function create($path, $content)
{
$resolvedPath = $this->resolvePath($path);
$this->ensureDir(dirname($resolvedPath));
try {
$stream = fopen($resolvedPath, 'x');
} catch (ErrorException $e) {
throw new AlreadyExistsException('Couldn\'t create the file "%s": %s', $path, $e);
}
try {
fclose($stream);
chmod($resolvedPath, 0664);
file_put_contents($resolvedPath, $content);
} catch (ErrorException $e) {
throw new NotWritableError('Couldn\'t create the file "%s": %s', $path, $e);
}
}
public function read($path)
{
$resolvedPath = $this->resolvePath($path, true);
try {
return file_get_contents($resolvedPath);
} catch (ErrorException $e) {
throw new NotReadableError('Couldn\'t read the file "%s": %s', $path, $e);
}
}
public function update($path, $content)
{
$resolvedPath = $this->resolvePath($path, true);
try {
file_put_contents($resolvedPath, $content);
} catch (ErrorException $e) {
throw new NotWritableError('Couldn\'t update the file "%s": %s', $path, $e);
}
}
public function delete($path)
{
$resolvedPath = $this->resolvePath($path, true);
try {
unlink($resolvedPath);
} catch (ErrorException $e) {
throw new NotWritableError('Couldn\'t delete the file "%s": %s', $path, $e);
}
}
public function resolvePath($path, $assertExistence = false)
{
if ($assertExistence && ! $this->has($path)) {
throw new NotFoundError('No such file: "%s"', $path);
}
$steps = preg_split('~/~', $path, -1, PREG_SPLIT_NO_EMPTY);
for ($i = 0; $i < count($steps);) {
if ($steps[$i] === '.') {
array_splice($steps, $i, 1);
} elseif ($steps[$i] === '..' && $i > 0 && $steps[$i - 1] !== '..') {
array_splice($steps, $i - 1, 2);
--$i;
} else {
++$i;
}
}
if ($steps[0] === '..') {
throw new InvalidArgumentException('Paths above the base directory are not allowed');
}
return $this->baseDir . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $steps);
}
/**
* Ensure that the given directory exists
*
* @param string $dir
*
* @throws NotWritableError
*/
protected function ensureDir($dir)
{
if (! is_dir($dir)) {
$this->ensureDir(dirname($dir));
try {
mkdir($dir, 02770);
} catch (ErrorException $e) {
throw new NotWritableError('Couldn\'t create the directory "%s": %s', $dir, $e);
}
}
}
}

View File

@ -0,0 +1,44 @@
<?php
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Storage;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* @deprecated This class will be removed once we require PHP 5.6
*/
class LocalFileStorageIterator extends RecursiveIteratorIterator
{
/**
* Constructor
*
* @param string $baseDir
*/
public function __construct($baseDir)
{
parent::__construct(new RecursiveDirectoryIterator($baseDir, RecursiveDirectoryIterator::SKIP_DOTS));
}
public function key()
{
parent::key();
return $this->current();
}
public function current()
{
/** @var RecursiveDirectoryIterator $innerIterator */
$innerIterator = $this->getInnerIterator();
/** @var \SplFileInfo $current */
$current = parent::current();
$subPath = $innerIterator->getSubPath();
return $subPath === ''
? $current->getFilename()
: str_replace(DIRECTORY_SEPARATOR, '/', $subPath) . '/' . $current->getFilename();
}
}

View File

@ -0,0 +1,94 @@
<?php
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Storage;
use Icinga\Exception\AlreadyExistsException;
use Icinga\Exception\NotFoundError;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use IteratorAggregate;
use Traversable;
interface StorageInterface extends IteratorAggregate
{
/**
* Iterate over all existing files' paths
*
* @return Traversable
*
* @throws NotReadableError If the file list can't be read
*/
public function getIterator();
/**
* Return whether the given file exists
*
* @param string $path
*
* @return bool
*/
public function has($path);
/**
* Create the given file with the given content
*
* @param string $path
* @param mixed $content
*
* @return $this
*
* @throws AlreadyExistsException If the file already exists
* @throws NotWritableError If the file can't be written to
*/
public function create($path, $content);
/**
* Load the content of the given file
*
* @param string $path
*
* @return mixed
*
* @throws NotFoundError If the file can't be found
* @throws NotReadableError If the file can't be read
*/
public function read($path);
/**
* Overwrite the given file with the given content
*
* @param string $path
* @param mixed $content
*
* @return $this
*
* @throws NotFoundError If the file can't be found
* @throws NotWritableError If the file can't be written to
*/
public function update($path, $content);
/**
* Delete the given file
*
* @param string $path
*
* @return $this
*
* @throws NotFoundError If the file can't be found
* @throws NotWritableError If the file can't be deleted
*/
public function delete($path);
/**
* Get the absolute path to the given file
*
* @param string $path
* @param bool $assertExistence Whether to require that the given file exists
*
* @return string
*
* @throws NotFoundError If the file has to exist, but can't be found
*/
public function resolvePath($path, $assertExistence = false);
}

View File

@ -0,0 +1,53 @@
<?php
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
namespace Icinga\File\Storage;
use ErrorException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
/**
* Stores files in a temporary directory
*/
class TemporaryLocalFileStorage extends LocalFileStorage
{
/**
* Constructor
*/
public function __construct()
{
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid();
mkdir($path, 0700);
parent::__construct($path);
}
/**
* Destructor
*/
public function __destruct()
{
$directoryIterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$this->baseDir,
RecursiveDirectoryIterator::CURRENT_AS_FILEINFO
| RecursiveDirectoryIterator::KEY_AS_PATHNAME
| RecursiveDirectoryIterator::SKIP_DOTS
),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($directoryIterator as $path => $entry) {
/** @var \SplFileInfo $entry */
if ($entry->isDir()) {
rmdir($path);
} else {
unlink($path);
}
}
rmdir($this->baseDir);
}
}

View File

@ -0,0 +1,212 @@
<?php
/* Icinga Web 2 | (c) 2017 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\File\Storage;
use ErrorException;
use Exception;
use Icinga\File\Storage\LocalFileStorage;
use Icinga\File\Storage\TemporaryLocalFileStorage;
use Icinga\Test\BaseTestCase;
class LocalFileStorageTest extends BaseTestCase
{
public function __construct($name = null, array $data = array(), $dataName = '')
{
parent::__construct($name, $data, $dataName);
error_reporting(E_ALL | E_STRICT);
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
if (error_reporting() === 0) {
// Error was suppressed with the @-operator
return false; // Continue with the normal error handler
}
switch ($errno) {
case E_NOTICE:
case E_WARNING:
case E_STRICT:
case E_RECOVERABLE_ERROR:
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
return false; // Continue with the normal error handler
});
}
public function testGetIterator()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
static::assertSame(array('foobar'), array_values(iterator_to_array($lfs->getIterator())));
}
/**
* @expectedException \Icinga\Exception\NotReadableError
*/
public function testGetIteratorThrowsNotReadableError()
{
$lfs = new LocalFileStorage('/notreadabledirectory');
$lfs->getIterator();
}
public function testHas()
{
$lfs = new TemporaryLocalFileStorage();
static::assertFalse($lfs->has('foobar'));
$lfs->create('foobar', 'Hello world!');
static::assertTrue($lfs->has('foobar'));
}
public function testCreate()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foo/bar', 'Hello world!');
static::assertSame('Hello world!', $lfs->read('foo/bar'));
}
/**
* @expectedException \Icinga\Exception\AlreadyExistsException
*/
public function testCreateThrowsAlreadyExistsException()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
$lfs->create('foobar', 'Hello world!');
}
/**
* @expectedException \Icinga\Exception\NotWritableError
*/
public function testCreateThrowsNotWritableError()
{
$lfs = new LocalFileStorage('/notwritabledirectory');
$lfs->create('foobar', 'Hello world!');
}
public function testRead()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
static::assertSame('Hello world!', $lfs->read('foobar'));
}
/**
* @expectedException \Icinga\Exception\NotFoundError
*/
public function testReadThrowsNotFoundError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->read('foobar');
}
/**
* @expectedException \Icinga\Exception\NotReadableError
*/
public function testReadThrowsNotReadableError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
chmod($lfs->resolvePath('foobar'), 0);
$lfs->read('foobar');
}
public function testUpdate()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
$lfs->update('foobar', 'Hello universe!');
static::assertSame('Hello universe!', $lfs->read('foobar'));
}
/**
* @expectedException \Icinga\Exception\NotFoundError
*/
public function testUpdateThrowsNotFoundError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->update('foobar', 'Hello universe!');
}
/**
* @expectedException \Icinga\Exception\NotWritableError
*/
public function testUpdateThrowsNotWritableError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
chmod($lfs->resolvePath('foobar'), 0);
$lfs->update('foobar', 'Hello universe!');
}
public function testDelete()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
$lfs->delete('foobar');
static::assertFalse($lfs->has('foobar'));
}
/**
* @expectedException \Icinga\Exception\NotFoundError
*/
public function testDeleteThrowsNotFoundError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->delete('foobar');
}
/**
* @expectedException \Icinga\Exception\NotWritableError
*/
public function testDeleteThrowsNotWritableError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
$baseDir = dirname($lfs->resolvePath('foobar'));
chmod($baseDir, 0500);
try {
$lfs->delete('foobar');
} catch (Exception $e) {
chmod($baseDir, 0700);
throw $e;
}
chmod($baseDir, 0700);
}
public function testResolvePath()
{
$lfs = new LocalFileStorage('/notreadabledirectory');
static::assertSame('/notreadabledirectory/foobar', $lfs->resolvePath('./notRelevant/../foobar'));
}
public function testResolvePathAssertExistence()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->create('foobar', 'Hello world!');
$lfs->resolvePath('./notRelevant/../foobar', true);
}
/**
* @expectedException \Icinga\Exception\NotFoundError
*/
public function testResolvePathThrowsNotFoundError()
{
$lfs = new TemporaryLocalFileStorage();
$lfs->resolvePath('foobar', true);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testResolvePathThrowsInvalidArgumentException()
{
$lfs = new LocalFileStorage('/notreadabledirectory');
$lfs->resolvePath('../foobar');
}
}