Implement local file storage
This commit is contained in:
parent
2caa07ce9b
commit
2cced5fe13
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue