diff --git a/library/Icinga/File/Storage/LocalFileStorage.php b/library/Icinga/File/Storage/LocalFileStorage.php new file mode 100644 index 000000000..4885f324e --- /dev/null +++ b/library/Icinga/File/Storage/LocalFileStorage.php @@ -0,0 +1,150 @@ +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); + } + } + } +} diff --git a/library/Icinga/File/Storage/LocalFileStorageIterator.php b/library/Icinga/File/Storage/LocalFileStorageIterator.php new file mode 100644 index 000000000..b530f4758 --- /dev/null +++ b/library/Icinga/File/Storage/LocalFileStorageIterator.php @@ -0,0 +1,44 @@ +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(); + } +} diff --git a/library/Icinga/File/Storage/StorageInterface.php b/library/Icinga/File/Storage/StorageInterface.php new file mode 100644 index 000000000..cdcec777a --- /dev/null +++ b/library/Icinga/File/Storage/StorageInterface.php @@ -0,0 +1,94 @@ +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); + } +} diff --git a/test/php/library/Icinga/File/Storage/LocalFileStorageTest.php b/test/php/library/Icinga/File/Storage/LocalFileStorageTest.php new file mode 100644 index 000000000..86ccc6f03 --- /dev/null +++ b/test/php/library/Icinga/File/Storage/LocalFileStorageTest.php @@ -0,0 +1,212 @@ +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'); + } +}