Add UrlCallBack wrapper that provides callbacks for fopen, unlink, rename, etc

This commit is contained in:
Robin Appelman 2014-08-27 15:37:52 +02:00
commit e5854bfd06
4 changed files with 421 additions and 0 deletions

104
src/Path.php Normal file
View file

@ -0,0 +1,104 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\Streams;
/**
* A string-like object that automatically registers a stream wrapper when used and removes the stream wrapper when no longer used
*
* Can optionally pass context options to the stream wrapper
*/
class Path {
/**
* @var bool
*/
protected $registered = false;
/**
* @var string
*/
protected $protocol;
/**
* @var string
*/
protected $class;
/**
* @var array
*/
protected $contextOptions;
/**
* @param string $class
* @param array $contextOptions
*/
public function __construct($class, $contextOptions = array()) {
$this->class = $class;
$this->contextOptions = $contextOptions;
}
public function getProtocol() {
if (!$this->protocol) {
$this->protocol = 'auto' . uniqid();
}
return $this->protocol;
}
public function wrapPath($path) {
return $this->getProtocol() . '://' . $path;
}
protected function register() {
if (!$this->registered) {
$this->appendDefaultContent($this->getProtocol(), $this->contextOptions);
stream_wrapper_register($this->getProtocol(), $this->class);
$this->registered = true;
}
}
protected function unregister() {
stream_wrapper_unregister($this->getProtocol());
$this->unsetDefaultContent($this->getProtocol());
$this->registered = false;
}
/**
* Add values to the default stream context
*
* @param string $key
* @param array $values
*/
protected function appendDefaultContent($key, $values) {
$context = stream_context_get_default();
$defaults = stream_context_get_options($context);
$defaults[$key] = $values;
stream_context_set_default($defaults);
}
/**
* Remove values from the default stream context
*
* @param string $key
*/
protected function unsetDefaultContent($key) {
$context = stream_context_get_default();
$defaults = stream_context_get_options($context);
unset($defaults[$key]);
stream_context_set_default($defaults);
}
public function __toString() {
$this->register();
return $this->protocol . '://';
}
public function __destruct() {
$this->unregister();
}
}

64
src/Url.php Normal file
View file

@ -0,0 +1,64 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\Streams;
/**
* Interface for stream wrappers that implement url functions such as unlink, stat
*/
interface Url {
/**
* @param string $path
* @param array $options
* @return bool
*/
public function dir_opendir($path, $options);
/**
* @param string $path
* @param string $mode
* @param int $options
* @param string &$opened_path
* @return bool
*/
public function stream_open($path, $mode, $options, &$opened_path);
/**
* @param string $path
* @param int $mode
* @param int $options
* @return bool
*/
public function mkdir($path, $mode, $options);
/**
* @param string $source
* @param string $target
* @return bool
*/
public function rename($source, $target);
/**
* @param string $path
* @param int $options
* @return bool
*/
public function rmdir($path, $options);
/**
* @param string
* @return bool
*/
public function unlink($path);
/**
* @param string $path
* @param int $flags
* @return array
*/
public function url_stat($path, $flags);
}

121
src/UrlCallBack.php Normal file
View file

@ -0,0 +1,121 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\Streams;
/**
* Wrapper that provides callbacks for url actions such as fopen, unlink, rename
*
* Usage:
*
* $path = UrlCallBack('/path/so/source', function(){
* echo 'fopen';
* }, function(){
* echo 'opendir';
* }, function(){
* echo 'mkdir';
* }, function(){
* echo 'rename';
* }, function(){
* echo 'rmdir';
* }, function(){
* echo 'unlink';
* }, function(){
* echo 'stat';
* });
*
* mkdir($path);
* ...
*
* All callbacks are called after the operation is executed on the source stream
*/
class UrlCallback extends Wrapper implements Url {
/**
* @param string $source
* @param callable $fopen
* @param callable $opendir
* @param callable $mkdir
* @param callable $rename
* @param callable $rmdir
* @param callable $unlink
* @param callable $stat
* @return \Icewind\Streams\Path
*
* @throws \BadMethodCallException
* @throws \Exception
*/
public static function wrap($source, $fopen = null, $opendir = null, $mkdir = null, $rename = null, $rmdir = null,
$unlink = null, $stat = null) {
$options = array(
'source' => $source,
'fopen' => $fopen,
'opendir' => $opendir,
'mkdir' => $mkdir,
'rename' => $rename,
'rmdir' => $rmdir,
'unlink' => $unlink,
'stat' => $stat
);
return new Path('\Icewind\Streams\UrlCallBack', $options);
}
protected function loadContext($url) {
list($protocol) = explode('://', $url);
$options = stream_context_get_options($this->context);
return $options[$protocol];
}
protected function callCallBack($context, $callback) {
if (is_callable($context[$callback])) {
call_user_func($context[$callback]);
}
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = $this->loadContext($path);
$this->callCallBack($context, 'fopen');
$this->setSourceStream(fopen($context['source'], $mode));
return true;
}
public function dir_opendir($path, $options) {
$context = $this->loadContext($path);
$this->callCallBack($context, 'opendir');
$this->setSourceStream(opendir($context['source']));
return true;
}
public function mkdir($path, $mode, $options) {
$context = $this->loadContext($path);
$this->callCallBack($context, 'mkdir');
return mkdir($context['source'], $mode, $options);
}
public function rmdir($path, $options) {
$context = $this->loadContext($path);
$this->callCallBack($context, 'rmdir');
return rmdir($context['source']);
}
public function rename($source, $target) {
$context = $this->loadContext($source);
$this->callCallBack($context, 'rename');
list(, $target) = explode('://', $target);
return rename($context['source'], $target);
}
public function unlink($path) {
$context = $this->loadContext($path);
$this->callCallBack($context, 'unlink');
return unlink($context['source']);
}
public function url_stat($path, $flags) {
throw new \Exception('stat is not supported due to php bug 50526');
}
}

132
tests/UrlCallBack.php Normal file
View file

@ -0,0 +1,132 @@
<?php
/**
* Copyright (c) 2014 Robin Appelman <icewind@owncloud.com>
* This file is licensed under the Licensed under the MIT license:
* http://opensource.org/licenses/MIT
*/
namespace Icewind\Streams\Tests;
class UrlCallBack extends \PHPUnit_Framework_TestCase {
protected $tempDirs = array();
protected function getTempDir() {
$dir = sys_get_temp_dir() . '/streams_' . uniqid();
mkdir($dir);
$this->tempDirs[] = $dir;
return $dir;
}
public function tearDown() {
foreach ($this->tempDirs as $dir) {
$this->rmdir($dir);
}
}
protected function rmdir($path) {
$directory = new \RecursiveDirectoryIterator($path);
$iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::CHILD_FIRST);
/**
* @var \SplFileInfo $file
*/
foreach ($iterator as $file) {
if (in_array($file->getBasename(), array('.', '..'))) {
continue;
} elseif ($file->isDir()) {
rmdir($file->getPathname());
} elseif ($file->isFile() || $file->isLink()) {
unlink($file->getPathname());
}
}
}
public function testFOpenCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$path = \Icewind\Streams\UrlCallBack::wrap('php://temp', $callback);
$fh = fopen($path, 'r');
fclose($fh);
$this->assertTrue($called);
}
public function testOpenDirCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$path = \Icewind\Streams\UrlCallBack::wrap($this->getTempDir(), null, $callback);
$fh = opendir($path);
closedir($fh);
$this->assertTrue($called);
}
public function testMKDirCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$dir = $this->getTempDir() . '/test';
$path = \Icewind\Streams\UrlCallBack::wrap($dir, null, null, $callback);
mkdir($path);
$this->assertTrue(file_exists($dir));
$this->assertTrue($called);
}
public function testRMDirCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$dir = $this->getTempDir() . '/test';
mkdir($dir);
$path = \Icewind\Streams\UrlCallBack::wrap($dir, null, null, null, null, $callback);
rmdir($path);
$this->assertFalse(file_exists($dir));
$this->assertTrue($called);
}
public function testRenameCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$source = $this->getTempDir() . '/test';
touch($source);
$path = \Icewind\Streams\UrlCallBack::wrap($source, null, null, null, $callback);
$target = $path->wrapPath($source . '_rename');
rename($path, $target);
$this->assertTrue(file_exists($source . '_rename'));
$this->assertTrue($called);
}
public function testUnlinkCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$file = $this->getTempDir() . '/test';
touch($file);
$path = \Icewind\Streams\UrlCallBack::wrap($file, null, null, null, null, null, $callback);
unlink($path);
$this->assertFalse(file_exists($file));
$this->assertTrue($called);
}
public function testStatCallBack() {
$called = false;
$callback = function () use (&$called) {
$called = true;
};
$file = $this->getTempDir() . '/test';
touch($file);
$path = \Icewind\Streams\UrlCallBack::wrap($file, null, null, null, null, null, null, $callback);
try {
stat($path);
} catch (\Exception $e) {
$this->markTestSkipped('url_stat doesn\'t receive the context parameter, see php bug 50526');
}
$this->assertTrue($called);
}
}