From 39de6ea2cfd621aaecb60185677449cd6e527aaa Mon Sep 17 00:00:00 2001 From: Daniel Lowrey Date: Fri, 10 Jul 2015 21:59:39 -0400 Subject: [PATCH] [WIP] initial code commit --- .gitignore | 2 + .php_cs | 15 ++ CONTRIBUTING.md | 33 ++++ README.md | 25 +++ composer.json | 45 +++++ lib/BlockingDescriptor.php | 90 +++++++++ lib/BlockingFilesystem.php | 169 ++++++++++++++++ lib/Descriptor.php | 49 +++++ lib/Filesystem.php | 146 ++++++++++++++ lib/UvDescriptor.php | 130 ++++++++++++ lib/UvFilesystem.php | 340 ++++++++++++++++++++++++++++++++ lib/functions.php | 30 +++ phpunit.xml | 35 ++++ test/BlockingDescriptorTest.php | 15 ++ test/BlockingFilesystemTest.php | 15 ++ test/DescriptorTest.php | 86 ++++++++ test/FilesystemTest.php | 129 ++++++++++++ test/UvDescriptorTest.php | 15 ++ test/UvFilesystemTest.php | 19 ++ test/fixture/small.txt | 1 + 20 files changed, 1389 insertions(+) create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 lib/BlockingDescriptor.php create mode 100644 lib/BlockingFilesystem.php create mode 100644 lib/Descriptor.php create mode 100644 lib/Filesystem.php create mode 100644 lib/UvDescriptor.php create mode 100644 lib/UvFilesystem.php create mode 100644 lib/functions.php create mode 100644 phpunit.xml create mode 100644 test/BlockingDescriptorTest.php create mode 100644 test/BlockingFilesystemTest.php create mode 100644 test/DescriptorTest.php create mode 100644 test/FilesystemTest.php create mode 100644 test/UvDescriptorTest.php create mode 100644 test/UvFilesystemTest.php create mode 100644 test/fixture/small.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8a7996 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +composer.lock +vendor/ diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..d1a2e36 --- /dev/null +++ b/.php_cs @@ -0,0 +1,15 @@ +level(Symfony\CS\FixerInterface::NONE_LEVEL) + ->fixers([ + "psr2", + "-braces", + "-psr0", + ]) + ->finder( + Symfony\CS\Finder\DefaultFinder::create() + ->in(__DIR__ . "/lib") + ->in(__DIR__ . "/test") + ) +; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b8bcb32 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +## Submitting useful bug reports + +Please search existing issues first to make sure this is not a duplicate. +Every issue report has a cost for the developers required to field it; be +respectful of others' time and ensure your report isn't spurious prior to +submission. Additionally, please do us all a favor by adhering to the +principles of sound bug reporting laid out by Simon Tatham here: + +http://www.chiark.greenend.org.uk/~sgtatham/bugs.html + +## Development ideology + +Truths which we believe to be self-evident: + +- **It's an asynchronous world.** Be wary of anything that undermines + async principles. + +- **The answer is not more options.** If you feel compelled to expose + new preferences to the user it's very possible you've made a wrong + turn somewhere. + +- **There are no power users.** The idea that some users "understand" + concepts better than others has proven to be, for the most part, false. + If anything, "power users" are more dangerous than the rest, and we + should avoid exposing dangerous functionality to them. + +## Code style + +The amphp project adheres to the PSR-2 style guide with the exception that +opening braces for classes and methods must appear on the same line as +the declaration: + +https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..99afa41 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# amp/fs [![Build Status](https://travis-ci.org/amphp/fs.svg?branch=master)](https://travis-ci.org/amphp/fs) + +`amp/fs` is a non-blocking filesystem manipulation library for use with the +[amp concurrency framework](https://github.com/amphp/amp). + +**Dependencies** + +- PHP 5.5+ +- [php-uv](https://github.com/chobie/php-uv) (optional) +- [eio](https://pecl.php.net/package/eio) (optional) + +`amp/fs` works out of the box without any PHP extensions. However, it does so +in a blocking manner. This capability only exists to simplify development across +environments where extensions may not be present. Using this library in +production without either the php-uv or eio extension is **NOT** reccommended. + +**Current Version** + + - v0.1.0 + +**Installation** + +```bash +$ composer require amphp/fs: ~0.1 +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..40007f8 --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "amphp/fs", + "homepage": "https://github.com/amphp/fs", + "description": "Non-blocking filesystem tools for amp applications", + "keywords": [ + "filesystem", + "static", + "async", + "non-blocking", + "amp", + "amphp" + ], + "license": "MIT", + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net", + "role": "Creator / Lead Developer" + } + ], + "require": { + "php": ">=7.0.0-dev", + "amphp/amp": "dev-master" + }, + "require-dev": { + "phpunit/phpunit": "~4.4.0", + "fabpot/php-cs-fixer": "~1.9" + }, + "autoload": { + "psr-4": { + "Amp\\Fs\\": "lib" + }, + "files": ["lib/functions.php"] + }, + "autoload-dev": { + "psr-4": { + "Amp\\Fs\\Test\\": "test/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.1.0-dev" + } + } +} diff --git a/lib/BlockingDescriptor.php b/lib/BlockingDescriptor.php new file mode 100644 index 0000000..44bb410 --- /dev/null +++ b/lib/BlockingDescriptor.php @@ -0,0 +1,90 @@ +fh = $fh; + $this->path = $path; + } + + /** + * {@inheritdoc} + */ + public function read(int $offset, int $len): Promise { + \fseek($this->fh, $offset); + $data = \fread($this->fh, $len); + + if ($data !== false) { + return new Success($data); + } else { + return new Failure(new \RuntimeException( + "Failed reading from file handle" + )); + } + } + + /** + * {@inheritdoc} + */ + public function write(int $offset, string $data): Promise { + \fseek($this->fh, $offset); + $len = \fwrite($this->fh, $data); + + if ($len !== false) { + return new Success($data); + } else { + return new Failure(new \RuntimeException( + "Failed writing to file handle" + )); + } + } + + /** + * {@inheritdoc} + */ + public function truncate(int $length = 0): Promise { + if (ftruncate($this->fh, $length)) { + return new Success; + } else { + return new Failure(new \RuntimeException( + "Failed truncating file handle" + )); + } + } + + /** + * {@inheritdoc} + */ + public function stat(): Promise { + if ($stat = fstat($this->fh)) { + $stat["isfile"] = (bool) is_file($this->path); + $stat["isdir"] = empty($stat["isfile"]); + return new Success($stat); + } else { + return new Failure(new \RuntimeException( + "File handle stat failed" + )); + } + } + + /** + * {@inheritdoc} + */ + public function close(): Promise { + if (\fclose($this->fh)) { + return new Success; + } else { + return new Failure(new \RuntimeException( + "Failed closing file handle" + )); + } + } +} diff --git a/lib/BlockingFilesystem.php b/lib/BlockingFilesystem.php new file mode 100644 index 0000000..cacef9d --- /dev/null +++ b/lib/BlockingFilesystem.php @@ -0,0 +1,169 @@ +reactor = $reactor ?: reactor(); + } + + /** + * {@inheritdoc} + */ + public function open(string $path, int $mode = self::READ): Promise { + $openMode = 0; + + if ($mode & self::READ && $mode & self::WRITE) { + $openMode = ($mode & self::CREATE) ? "c+" : "r+"; + } elseif ($mode & self::READ) { + $openMode = "r"; + } elseif ($mode & self::WRITE) { + $openMode = "c"; + } else { + return new Failure(new \InvalidArgumentException( + "Invalid file open mode: Filesystem::READ or Filesystem::WRITE or both required" + )); + } + + if ($fh = @fopen($path, $openMode)) { + $descriptor = new BlockingDescriptor($fh, $path); + return new Success($descriptor); + } else { + return new Failure(new \RuntimeException( + "Failed opening file handle" + )); + } + } + + /** + * {@inheritdoc} + */ + public function stat(string $path): Promise { + if ($stat = @stat($path)) { + $stat["isfile"] = (bool) is_file($path); + $stat["isdir"] = empty($stat["isfile"]); + clearstatcache(true, $path); + } else { + $stat = null; + } + + return new Success($stat); + } + + /** + * {@inheritdoc} + */ + public function lstat(string $path): Promise { + if ($stat = @lstat($path)) { + $stat["isfile"] = (bool) is_file($path); + $stat["isdir"] = empty($stat["isfile"]); + clearstatcache(true, $path); + } else { + $stat = null; + } + + return new Success($stat); + } + + /** + * {@inheritdoc} + */ + public function symlink(string $target, string $link): Promise { + return new Success((bool) symlink($target, $link)); + } + + /** + * {@inheritdoc} + */ + public function rename(string $from, string $to): Promise { + return new Success((bool) rename($from, $to)); + } + + /** + * {@inheritdoc} + */ + public function unlink(string $path): Promise { + return new Success((bool) unlink($path)); + } + + /** + * {@inheritdoc} + */ + public function mkdir(string $path, int $mode = 0644): Promise { + return new Success((bool) mkdir($path, $mode)); + } + + /** + * {@inheritdoc} + */ + public function rmdir(string $path): Promise { + return new Success((bool) rmdir($path)); + } + + /** + * {@inheritdoc} + */ + public function scandir(string $path): Promise { + if ($arr = scandir($path)) { + $arr = array_values(array_filter($arr, function($el) { + return !($el === "." || $el === ".."); + })); + clearstatcache(true, $path); + return new Success($arr); + } else { + return new Failure(new \RuntimeException( + "Failed reading contents from {$path}" + )); + } + } + + /** + * {@inheritdoc} + */ + public function chmod(string $path, int $mode): Promise { + return new Success((bool) chmod($path, $mode)); + } + + /** + * {@inheritdoc} + */ + public function chown(string $path, int $uid, int $gid): Promise { + if (!@chown($path, $uid)) { + return new Failure(new \RuntimeException( + error_get_last()["message"] + )); + } elseif (!@chgrp($path, $gid)) { + return new Failure(new \RuntimeException( + error_get_last()["message"] + )); + } else { + return new Success; + } + } + + /** + * {@inheritdoc} + */ + public function get(string $path): Promise { + $result = @file_get_contents($path); + return ($result === false) + ? new Failure(new \RuntimeException(error_get_last()["message"])) + : new Success($result) + ; + } + + /** + * {@inheritdoc} + */ + public function put(string $path, string $contents): Promise { + $result = @file_put_contents($path, $contents); + return ($result === false) + ? new Failure(new \RuntimeException(error_get_last()["message"])) + : new Success($result) + ; + } +} diff --git a/lib/Descriptor.php b/lib/Descriptor.php new file mode 100644 index 0000000..6cd70eb --- /dev/null +++ b/lib/Descriptor.php @@ -0,0 +1,49 @@ +open("/path/to/file", $mode); + * assert($fh instanceof Descriptor); + * + * + * NOTE: This operation is only valid for files (not directories). + * + * @param string $path The filesystem path to open + * @param int $mode A flag bitmask: [Filesystem::READ | Filesystem::WRITE | Filesystem::CREATE] + * @return \Amp\Promise + */ + public function open(string $path, int $mode = self::READ): Promise; + + /** + * Execute a file stat operation + * + * If the requested path does not exist the resulting Promise will resolve to NULL. + * + * @param string $path The file system path to stat + * @return \Amp\Promise A promise resolving to an associative array upon successful resolution + */ + public function stat(string $path): Promise; + + /** + * Same as stat() except if the path is a link then the link's data is returned + * + * @param string $path The file system path to stat + * @return \Amp\Promise A promise resolving to an associative array upon successful resolution + */ + public function lstat(string $path): Promise; + + /** + * Create a symlink $link pointing to the file/directory located at $target + * + * @param string $target + * @param string $link + * @return \Amp\Promise + */ + public function symlink(string $target, string $link): Promise; + + /** + * Rename a file or directory + * + * @param string $from + * @param string $to + * @return \Amp\Promise + */ + public function rename(string $from, string $to): Promise; + + /** + * Delete a file + * + * @param string $path + * @return \Amp\Promise + */ + public function unlink(string $path): Promise; + + /** + * Create a director + * + * @param string $path + * @param int $mode + * @return \Amp\Promise + */ + public function mkdir(string $path, int $mode = 0644): Promise; + + /** + * Delete a directory + * + * @param string $path + * @return \Amp\Promise + */ + public function rmdir(string $path): Promise; + + /** + * Retrieve an array of files and directories inside the specified path + * + * Dot entries are not included in the resulting array (i.e. "." and ".."). + * + * @param string $path + * @return \Amp\Promise + */ + public function scandir(string $path): Promise; + + /** + * chmod a file or directory + * + * @param string $path + * @param int $mode + * @return \Amp\Promise + */ + public function chmod(string $path, int $mode): Promise; + + /** + * chown a file or directory + * + * @param string $path + * @param int $uid + * @param int $gid + * @return \Amp\Promise + */ + public function chown(string $path, int $uid, int $gid): Promise; + + /** + * Buffer the specified file's contents + * + * @param string $path The file path from which to buffer contents + * @return \Amp\Promise A promise resolving to a string upon successful resolution + */ + public function get(string $path): Promise; + + /** + * Write the contents string to the specified path. + * + * @param string $path The file path to which to $contents should be written + * @param string $contents The data to write to the specified $path + * @return \Amp\Promise A promise resolving to the integer length written upon success + */ + public function put(string $path, string $contents): Promise; +} diff --git a/lib/UvDescriptor.php b/lib/UvDescriptor.php new file mode 100644 index 0000000..09a98a0 --- /dev/null +++ b/lib/UvDescriptor.php @@ -0,0 +1,130 @@ +reactor = $reactor; + $this->fh = $fh; + $this->loop = $reactor->getUnderlyingLoop(); + } + + public function __destruct() { + if (empty($this->isCloseInitialized)) { + $this->close(); + } + } + + /** + * {@inheritdoc} + */ + public function read(int $offset, int $len): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_read($this->loop, $this->fh, $offset, $len, function($fh, $result, $buf) use ($promisor) { + $this->reactor->delRef(); + if ($result < 0) { + $promisor->fail(new \RuntimeException( + "Failed reading from file handle: " . \uv_strerror($result) + )); + } else { + $promisor->succeed($buf); + } + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function write(int $offset, string $data): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_write($this->loop, $this->fh, $data, $offset, function($fh, $result) use ($promisor) { + $this->reactor->delRef(); + if ($result < 0) { + $promisor->fail(new \RuntimeException( + "Failed writing to file handle: " . \uv_strerror($result) + )); + } else { + $promisor->succeed($result); + } + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function truncate(int $length = 0): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_ftruncate($this->loop, $this->fh, $length, function($fh) use ($promisor) { + $this->reactor->delRef(); + if (empty($fh)) { + $promisor->fail(new \RuntimeException( + "Failed truncating file handle" + )); + } else { + $promisor->succeed(); + } + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function stat(): Promise { + // @TODO Pull result from stat cache if it exists + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_fstat($this->loop, $this->fh, function($fh, $stat) use ($promisor) { + if ($fh) { + $stat["isdir"] = (bool) ($stat["mode"] & \UV::S_IFDIR); + $stat["isfile"] = empty($stat["isdir"]); + } else { + $stat = null; + } + $this->reactor->delRef(); + $promisor->succeed($stat); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function close(): Promise { + $this->isCloseInitialized = true; + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_close($this->loop, $this->fh, function($fh) use ($promisor) { + $this->reactor->delRef(); + if (empty($fh)) { + $promisor->fail(new \RuntimeException( + "Failed closing file handle" + )); + } else { + $promisor->succeed(); + } + }); + + return $promisor->promise(); + } +} diff --git a/lib/UvFilesystem.php b/lib/UvFilesystem.php new file mode 100644 index 0000000..c4989b4 --- /dev/null +++ b/lib/UvFilesystem.php @@ -0,0 +1,340 @@ +reactor = $reactor; + $this->loop = $this->reactor->getUnderlyingLoop(); + } + + /** + * {@inheritdoc} + */ + public function open(string $path, int $mode = self::READ): Promise { + $openFlags = 0; + $fileChmod = 0; + + if ($mode & self::READ && $mode & self::WRITE) { + $openFlags = \UV::O_RDWR; + $mode |= self::CREATE; + } elseif ($mode & self::READ) { + $openFlags = \UV::O_RDONLY; + } elseif ($mode & self::WRITE) { + $openFlags = \UV::O_WRONLY; + $mode |= self::CREATE; + } else { + return new Failure(new \InvalidArgumentException( + "Invalid file open mode: Filesystem::READ or Filesystem::WRITE or both required" + )); + } + + if ($mode & self::CREATE) { + $openFlags |= \UV::O_CREAT; + $fileChmod = 0644; + } + + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_open($this->loop, $path, $openFlags, $fileChmod, function($fh) use ($promisor) { + $this->reactor->delRef(); + if ($fh) { + $descriptor = new UvDescriptor($this->reactor, $fh); + $promisor->succeed($descriptor); + } else { + $promisor->fail(new \RuntimeException( + "Failed opening file handle" + )); + } + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function stat(string $path): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_stat($this->loop, $path, function($fh, $stat) use ($promisor) { + if ($fh) { + $stat["isdir"] = (bool) ($stat["mode"] & \UV::S_IFDIR); + $stat["isfile"] = empty($stat["isdir"]); + } else { + $stat = null; + } + $this->reactor->delRef(); + $promisor->succeed($stat); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function lstat(string $path): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_lstat($this->loop, $path, function($fh, $stat) use ($promisor) { + if ($fh) { + $stat["isdir"] = (bool) ($stat["mode"] & \UV::S_IFDIR); + $stat["isfile"] = empty($stat["isdir"]); + } else { + $stat = null; + } + $this->reactor->delRef(); + $promisor->succeed($stat); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function symlink(string $target, string $link): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_symlink($this->loop, $target, $link, \UV::S_IRWXU | \UV::S_IRUSR, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function rename(string $from, string $to): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_rename($this->loop, $from, $to, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function unlink(string $path): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_unlink($this->loop, $path, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function mkdir(string $path, int $mode = 0644): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_mkdir($this->loop, $path, $mode, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function rmdir(string $path): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_rmdir($this->loop, $path, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function scandir(string $path): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + uv_fs_readdir($this->loop, $path, 0, function($fh, $data) use ($promisor, $path) { + $this->reactor->delRef(); + if (empty($fh)) { + $promisor->fail(new \RuntimeException( + "Failed reading contents from {$path}" + )); + } else { + $promisor->succeed($data); + } + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function chmod(string $path, int $mode): Promise { + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_chmod($this->loop, $path, $mode, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function chown(string $path, int $uid, int $gid): Promise { + // @TODO Return a failure in windows environments + $this->reactor->addRef(); + $promisor = new Deferred; + \uv_fs_chown($this->loop, $path, $uid, $gid, function($fh) use ($promisor) { + $this->reactor->delRef(); + $promisor->succeed((bool)$fh); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function get(string $path): Promise { + return resolve($this->doGet($path), $this->reactor); + } + + private function doGet(string $path): \Generator { + $this->reactor->addRef(); + if (!$fh = yield $this->doFsOpen($path, $flags = \UV::O_RDONLY, $mode = 0)) { + $this->reactor->delRef(); + throw new \RuntimeException( + "Failed opening file handle: {$path}" + ); + } + + $promisor = new Deferred; + $stat = yield $this->doFsStat($fh); + if (empty($stat)) { + $this->reactor->delRef(); + $promisor->fail(new \RuntimeException( + "stat operation failed on open file handle" + )); + } elseif (!$stat["isfile"]) { + \uv_fs_close($this->loop, $fh, function() use ($promisor) { + $this->reactor->delRef(); + $promisor->fail(new \RuntimeException( + "cannot buffer contents: path is not a file" + )); + }); + } else { + $buffer = yield $this->doFsRead($fh, $offset = 0, $stat["size"]); + if ($buffer === false ) { + \uv_fs_close($this->loop, $fh, function() use ($promisor) { + $this->reactor->delRef(); + $promisor->fail(new \RuntimeException( + "read operation failed on open file handle" + )); + }); + } else { + \uv_fs_close($this->loop, $fh, function() use ($promisor, $buffer) { + $this->reactor->delRef(); + $promisor->succeed($buffer); + }); + } + } + + return yield $promisor->promise(); + } + + private function doFsOpen(string $path, int $flags, int $mode): Promise { + $promisor = new Deferred; + \uv_fs_open($this->loop, $path, $flags, $mode, function($fh) use ($promisor, $path) { + $promisor->succeed($fh); + }); + + return $promisor->promise(); + } + + private function doFsStat($fh): Promise { + $promisor = new Deferred; + \uv_fs_fstat($this->loop, $fh, function($fh, $stat) use ($promisor) { + if ($fh) { + $stat["isdir"] = (bool) ($stat["mode"] & \UV::S_IFDIR); + $stat["isfile"] = !$stat["isdir"]; + $promisor->succeed($stat); + } else { + $promisor->succeed(); + } + }); + + return $promisor->promise(); + } + + private function doFsRead($fh, int $offset, int $len): Promise { + $promisor = new Deferred; + \uv_fs_read($this->loop, $fh, $offset, $len, function($fh, $nread, $buffer) use ($promisor) { + $promisor->succeed(($nread < 0) ? false : $buffer); + }); + + return $promisor->promise(); + } + + /** + * {@inheritdoc} + */ + public function put(string $path, string $contents): Promise { + return resolve($this->doPut($path, $contents), $this->reactor); + } + + private function doPut(string $path, string $contents): \Generator { + $flags = \UV::O_WRONLY | \UV::O_CREAT; + $mode = \UV::S_IRWXU | \UV::S_IRUSR; + $this->reactor->addRef(); + if (!$fh = yield $this->doFsOpen($path, $flags, $mode)) { + $this->reactor->delRef(); + throw new \RuntimeException( + "Failed opening write file handle" + ); + } + + $promisor = new Deferred; + $len = strlen($contents); + \uv_fs_write($this->loop, $fh, $contents, $offset = 0, function($fh, $result) use ($promisor, $len) { + \uv_fs_close($this->loop, $fh, function() use ($promisor, $result, $len) { + $this->reactor->delRef(); + if ($result < 0) { + $promisor->fail(new \RuntimeException( + uv_strerror($result) + )); + } else { + $promisor->succeed($len); + } + }); + }); + + return yield $promisor->promise(); + } +} diff --git a/lib/functions.php b/lib/functions.php new file mode 100644 index 0000000..a565e29 --- /dev/null +++ b/lib/functions.php @@ -0,0 +1,30 @@ + + + + + + ./test + + + + + ./lib + + + + + + + + + diff --git a/test/BlockingDescriptorTest.php b/test/BlockingDescriptorTest.php new file mode 100644 index 0000000..85e78ff --- /dev/null +++ b/test/BlockingDescriptorTest.php @@ -0,0 +1,15 @@ +getReactor())->run(function($reactor) { + $path = __DIR__ . "/fixture/new.txt"; + $fs = $this->getFilesystem($reactor); + $flags = Filesystem::READ | Filesystem::WRITE | Filesystem::CREATE; + $fh = yield $fs->open($path, $flags); + yield $fh->write(0, "test"); + $data = yield $fh->read(0, 8192); + $this->assertSame("test", $data); + yield $fh->close(); + yield $fs->unlink($path); + }); + } + + /** + * @expectedException RuntimeException + */ + public function testWriteFailsOnDirectory() { + ($this->getReactor())->run(function($reactor) { + $path = __DIR__ . "/fixture/dir"; + $fs = $this->getFilesystem($reactor); + $flags = Filesystem::READ | Filesystem::WRITE | Filesystem::CREATE; + $fh = yield $fs->open($path, $flags); + yield $fh->write(0, "should fail because this is a directory"); + }); + } + + /** + * @expectedException RuntimeException + */ + public function testReadFailsOnDirectory() { + ($this->getReactor())->run(function($reactor) { + $path = __DIR__ . "/fixture/dir"; + $fs = $this->getFilesystem($reactor); + $flags = Filesystem::READ | Filesystem::WRITE | Filesystem::CREATE; + $fh = yield $fs->open($path, $flags); + yield $fh->read(0, 8192); + }); + } + + public function testTruncate() { + ($this->getReactor())->run(function($reactor) { + $path = __DIR__ . "/fixture/truncate.txt"; + $fs = $this->getFilesystem($reactor); + $flags = Filesystem::READ | Filesystem::WRITE | Filesystem::CREATE; + $fh = yield $fs->open($path, $flags); + yield $fh->write(0, "test"); + $data = yield $fh->read(0, 8192); + $this->assertSame("test", $data); + yield $fh->truncate(); + yield $fh->close(); + + $this->assertEquals(0, (yield $fs->stat($path))["size"]); + yield $fs->unlink($path); + }); + } + + public function testStat() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + // file + $fh = yield $fs->open(__DIR__ . "/fixture/small.txt"); + $stat = yield $fh->stat(); + $this->assertTrue($stat["isfile"]); + $this->assertFalse($stat["isdir"]); + + // directory + $fh = yield $fs->open(__DIR__ . "/fixture/dir"); + $stat = yield $fh->stat(); + $this->assertFalse($stat["isfile"]); + $this->assertTrue($stat["isdir"]); + }); + } +} diff --git a/test/FilesystemTest.php b/test/FilesystemTest.php new file mode 100644 index 0000000..e707daf --- /dev/null +++ b/test/FilesystemTest.php @@ -0,0 +1,129 @@ +getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + $descriptor = yield $fs->open(__DIR__ . "/fixture/small.txt", Filesystem::READ); + $this->assertInstanceOf("Amp\Fs\Descriptor", $descriptor); + }); + } + + public function testScandir() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + $actual = yield $fs->scandir(__DIR__ . "/fixture"); + $expected = ["dir", "small.txt"]; + $this->assertSame($expected, $actual); + }); + } + + public function testSymlink() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + $target = __DIR__ . "/fixture/small.txt"; + $link = __DIR__ . "/fixture/symlink.txt"; + $this->assertTrue(yield $fs->symlink($target, $link)); + $this->assertTrue(is_link($link)); + yield $fs->unlink($link); + }); + } + + public function testLstat() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + $target = __DIR__ . "/fixture/small.txt"; + $link = __DIR__ . "/fixture/symlink.txt"; + $this->assertTrue(yield $fs->symlink($target, $link)); + $this->assertTrue(is_array(yield $fs->lstat($link))); + yield $fs->unlink($link); + }); + } + + /** + * @expectedException RuntimeException + */ + public function testOpenFailsOnNonexistentFile() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + $descriptor = yield $fs->open(__DIR__ . "/fixture/nonexistent", Filesystem::READ); + $this->assertInstanceOf("Amp\Fs\Descriptor", $descriptor); + }); + } + + public function testStat() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + // file + $stat = yield $fs->stat(__DIR__ . "/fixture/small.txt"); + $this->assertTrue($stat["isfile"]); + $this->assertFalse($stat["isdir"]); + + // directory + $stat = yield $fs->stat(__DIR__ . "/fixture/dir"); + $this->assertFalse($stat["isfile"]); + $this->assertTrue($stat["isdir"]); + + // nonexistent + $stat = yield $fs->stat(__DIR__ . "/fixture/nonexistent"); + $this->assertNull($stat); + }); + } + + public function testRename() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + $contents1 = "rename test"; + $old = __DIR__ . "/fixture/rename1.txt"; + $new = __DIR__ . "/fixture/rename2.txt"; + + yield $fs->put($old, $contents1); + yield $fs->rename($old, $new); + $contents2 = yield $fs->get($new); + yield $fs->unlink($new); + + $this->assertSame($contents1, $contents2); + }); + } + + public function testUnlink() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + $toUnlink = __DIR__ . "/fixture/unlink"; + + yield $fs->put($toUnlink, ""); + $this->assertTrue((bool) yield $fs->stat($toUnlink)); + yield $fs->unlink($toUnlink); + $this->assertNull(yield $fs->stat($toUnlink)); + }); + } + + public function testMkdirRmdir() { + ($this->getReactor())->run(function($reactor) { + $fs = $this->getFilesystem($reactor); + + $dir = __DIR__ . "/fixture/newdir"; + + yield $fs->mkdir($dir); + $stat = yield $fs->stat($dir); + $this->assertTrue($stat["isdir"]); + $this->assertFalse($stat["isfile"]); + yield $fs->rmdir($dir); + $this->assertNull(yield $fs->stat($dir)); + }); + } + +} diff --git a/test/UvDescriptorTest.php b/test/UvDescriptorTest.php new file mode 100644 index 0000000..2542642 --- /dev/null +++ b/test/UvDescriptorTest.php @@ -0,0 +1,15 @@ +markTestSkipped("currently crashes php"); + } +} diff --git a/test/fixture/small.txt b/test/fixture/small.txt new file mode 100644 index 0000000..64c3ecd --- /dev/null +++ b/test/fixture/small.txt @@ -0,0 +1 @@ +small \ No newline at end of file