1
0
mirror of https://github.com/danog/file.git synced 2024-11-29 20:09:10 +01:00

[WIP] initial code commit

This commit is contained in:
Daniel Lowrey 2015-07-10 21:59:39 -04:00
parent 40b2dbcf49
commit 39de6ea2cf
20 changed files with 1389 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
composer.lock
vendor/

15
.php_cs Normal file
View File

@ -0,0 +1,15 @@
<?php
return Symfony\CS\Config\Config::create()
->level(Symfony\CS\FixerInterface::NONE_LEVEL)
->fixers([
"psr2",
"-braces",
"-psr0",
])
->finder(
Symfony\CS\Finder\DefaultFinder::create()
->in(__DIR__ . "/lib")
->in(__DIR__ . "/test")
)
;

33
CONTRIBUTING.md Normal file
View File

@ -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

25
README.md Normal file
View File

@ -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
```

45
composer.json Normal file
View File

@ -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"
}
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace Amp\Fs;
use Amp\{ Promise, Success, Failure };
class BlockingDescriptor implements Descriptor {
private $fh;
/**
* @param resource $fh An open uv filesystem descriptor
*/
public function __construct($fh, string $path) {
$this->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"
));
}
}
}

169
lib/BlockingFilesystem.php Normal file
View File

@ -0,0 +1,169 @@
<?php
namespace Amp\Fs;
use Amp\{ Reactor, function reactor, Promise, Success, Failure };
class BlockingFilesystem implements Filesystem {
private $reactor;
public function __construct(Reactor $reactor = null) {
$this->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)
;
}
}

49
lib/Descriptor.php Normal file
View File

@ -0,0 +1,49 @@
<?php
namespace Amp\Fs;
use Amp\Promise;
interface Descriptor {
/**
* Read $len bytes from the open file handle starting at $offset
*
* @param int $offset
* @param int $len
* @return \Amp\Promise
*/
public function read(int $offset, int $len): Promise;
/**
* Write $data to the open file handle starting at $offset
*
* @param int $offset
* @param string $data
* @return \Amp\Promise
*/
public function write(int $offset, string $data): Promise;
/**
* Truncate the file to the specified $length
*
* Note: The descriptor must be opened for writing
*
* @param int $length
* @return \Amp\Promise
*/
public function truncate(int $length = 0): Promise;
/**
* Retrieve the filesystem stat array for the current descriptor
*
* @return \Amp\Promise
*/
public function stat(): Promise;
/**
* Close the file handle
*
* @return \Amp\Promise
*/
public function close(): Promise;
}

146
lib/Filesystem.php Normal file
View File

@ -0,0 +1,146 @@
<?php
namespace Amp\Fs;
use Amp\Promise;
interface Filesystem {
const READ = 0b001;
const WRITE = 0b010;
const CREATE = 0b100;
/**
* Open a file handle for reading and/or writing
*
* At least READ or WRITE is required in the mode bitmask. If the file does not exist the
* CREATE flag is necessary in READ mode or the operation will fail. When WRITE mode is
* specified in the bitmask the file will always be created if it does not already exist.
*
* Example:
*
* <?php
* use function Amp\Fs\fs();
* use Amp\Fs\{ Filesystem, Descriptor };
*
* function() {
* $fs = fs();
* $mode = Filesystem::READ | Filesystem::WRITE;
* $fh = yield $fs->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;
}

130
lib/UvDescriptor.php Normal file
View File

@ -0,0 +1,130 @@
<?php
namespace Amp\Fs;
use Amp\{ Promise, Deferred, UvReactor };
class UvDescriptor implements Descriptor {
private $reactor;
private $fh;
private $loop;
private $isCloseInitialized = false;
/**
* @param \Amp\UvReactor $reactor
* @param resource $fh An open uv filesystem descriptor
*/
public function __construct(UvReactor $reactor, $fh) {
$this->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();
}
}

340
lib/UvFilesystem.php Normal file
View File

@ -0,0 +1,340 @@
<?php
namespace Amp\Fs;
use Amp\{ UvReactor, Promise, Failure, Deferred };
use function Amp\{ resolve, reactor };
class UvFilesystem implements Filesystem {
private $reactor;
private $loop;
/**
* @param \Amp\UvReactor $reactor
*/
public function __construct(UvReactor $reactor) {
$this->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();
}
}

30
lib/functions.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace Amp\Fs;
use function Amp\reactor;
/**
* Get the global default filesystem instance
*
* @param \Amp\Fs\Filesystem $assign Optionally specify a new default filesystem instance
* @return \Amp\Fs\Filesystem Returns the default filesystem instance
*/
function fs(Filesystem $assign = null): Filesystem {
static $filesystem;
if ($assign) {
return ($filesystem = $assign);
} elseif ($filesystem) {
return $filesystem;
} elseif (\extension_loaded('uv')) {
return ($filesystem = new UvFilesystem(reactor()));
/*
// @TODO
} elseif (\extension_loaded("eio") {
return ($filesystem = new EioFilesystem);
}
*/
} else {
return ($filesystem = new BlockingFilesystem(reactor()));
}
}

35
phpunit.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory>./lib</directory>
</whitelist>
</filter>
<logging>
<!--
<log
type="coverage-html"
target="./test/coverage"
charset="UTF-8"
yui="true"
lowUpperBound="35"
highLowerBound="70"
showUncoveredFiles="true"
/>
-->
<log
type="coverage-text"
target="php://stdout"
lowUpperBound="35"
highLowerBound="70"
/>
</logging>
</phpunit>

View File

@ -0,0 +1,15 @@
<?php
namespace Amp\Fs\Test;
use Amp\{ Reactor, NativeReactor };
use Amp\Fs\{ Filesystem, BlockingFilesystem };
class BlockingDescriptorTest extends DescriptorTest {
protected function getReactor(): Reactor {
return new NativeReactor;
}
protected function getFilesystem(Reactor $reactor): Filesystem {
return new BlockingFilesystem($reactor);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Amp\Fs\Test;
use Amp\{ Reactor, NativeReactor };
use Amp\Fs\{ Filesystem, BlockingFilesystem };
class BlockingFilesystemTest extends FilesystemTest {
protected function getReactor(): Reactor {
return new NativeReactor;
}
protected function getFilesystem(Reactor $reactor): Filesystem {
return new BlockingFilesystem($reactor);
}
}

86
test/DescriptorTest.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace Amp\Fs\Test;
use Amp\Reactor;
use Amp\Fs\Filesystem;
abstract class DescriptorTest extends \PHPUnit_Framework_TestCase {
abstract protected function getReactor(): Reactor;
abstract protected function getFilesystem(Reactor $reactor): Filesystem;
public function testReadWriteCreate() {
($this->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"]);
});
}
}

129
test/FilesystemTest.php Normal file
View File

@ -0,0 +1,129 @@
<?php
namespace Amp\Fs\Test;
use Amp\Reactor;
use Amp\Fs\Filesystem;
abstract class FilesystemTest extends \PHPUnit_Framework_TestCase {
abstract protected function getReactor(): Reactor;
abstract protected function getFilesystem(Reactor $reactor): Filesystem;
public function testOpen() {
($this->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));
});
}
}

15
test/UvDescriptorTest.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace Amp\Fs\Test;
use Amp\{ Reactor, UvReactor };
use Amp\Fs\{ Filesystem, UvFilesystem };
class UvDescriptorTest extends DescriptorTest {
protected function getReactor(): Reactor {
return new UvReactor;
}
protected function getFilesystem(Reactor $reactor): Filesystem {
return new UvFilesystem($reactor);
}
}

19
test/UvFilesystemTest.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace Amp\Fs\Test;
use Amp\{ Reactor, UvReactor };
use Amp\Fs\{ Filesystem, UvFilesystem };
class UvFilesystemTest extends FilesystemTest {
protected function getReactor(): Reactor {
return new UvReactor;
}
protected function getFilesystem(Reactor $reactor): Filesystem {
return new UvFilesystem($reactor);
}
public function testScandir() {
$this->markTestSkipped("currently crashes php");
}
}

1
test/fixture/small.txt Normal file
View File

@ -0,0 +1 @@
small