Skip to content

Commit

Permalink
Merge pull request #112 from WyriHaximus-labs/0.2.x-reointroduce-eio-…
Browse files Browse the repository at this point in the history
…adapter

Reintroduce the ext-eio adapter
  • Loading branch information
WyriHaximus committed May 30, 2023
2 parents 57d8f3a + e469054 commit ca5e06f
Show file tree
Hide file tree
Showing 13 changed files with 564 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ jobs:
extensions:
- ""
- "uv-amphp/ext-uv@master"
- "uv-amphp/ext-uv@master, eio"
- "eio"
steps:
- uses: actions/checkout@v2
- name: Install libuv
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install libuv1-dev
- name: Install ext-eio
if: matrix.os == 'ubuntu-latest' && (matrix.extensions == 'eio' || matrix.extensions == 'uv-amphp/ext-uv@master, eio')
run: sudo pecl install eio || sudo pecl install eio-beta
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* [Factory](#factory)
* [create()](#create)
* [Filesystem implementations](#filesystem-implementations)
* [Eio](#eio)
* [Uv](#uv)
* [AdapterInterface](#adapterinterface)
* [detect()](#detect)
Expand Down Expand Up @@ -110,6 +111,15 @@ manually instantiate one of the following classes.
Note that you may have to install the required PHP extensions for the respective
event loop implementation first or they will throw a `BadMethodCallException` on creation.

#### Eio

An `ext-eio` based filesystem.

This filesystem uses the [`eio` PECL extension](https://pecl.php.net/package/eio), that
provides an interface to `libeio` library.

This filesystem is known to work with PHP 7+.

#### Uv

An `ext-uv` based filesystem.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
}
},
"suggest": {
"ext-eio": "* for great I/O performance",
"ext-uv": "* for better I/O performance"
},
"config": {
Expand Down
67 changes: 67 additions & 0 deletions src/Eio/Adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace React\Filesystem\Eio;

use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Filesystem\AdapterInterface;
use React\Filesystem\ModeTypeDetector;
use React\Filesystem\Node;
use React\Filesystem\PollInterface;
use React\Filesystem\Stat;
use React\Promise\PromiseInterface;

final class Adapter implements AdapterInterface
{
use StatTrait;

private LoopInterface $loop;
private PollInterface $poll;

public function __construct()
{
$this->loop = Loop::get();
$this->poll = new Poll($this->loop);
}

public function detect(string $path): PromiseInterface
{
return $this->internalStat($path)->then(function (?Stat $stat) use ($path) {
if ($stat === null) {
return new NotExist($this->poll, $this, $this->loop, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
}

switch (ModeTypeDetector::detect($stat->mode())) {
case Node\FileInterface::class:
return $this->file($stat->path());
break;
case Node\DirectoryInterface::class:
return $this->directory($stat->path());
break;
default:
return new Node\Unknown($stat->path(), $stat->path());
break;
}
});
}

public function directory(string $path): Node\DirectoryInterface
{
return new Directory($this->poll, $this, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
}

public function file(string $path): Node\FileInterface
{
return new File($this->poll, dirname($path) . DIRECTORY_SEPARATOR, basename($path));
}

protected function activate(): void
{
$this->poll->activate();
}

protected function deactivate(): void
{
$this->poll->deactivate();
}
}
101 changes: 101 additions & 0 deletions src/Eio/Directory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace React\Filesystem\Eio;

use React\Filesystem\AdapterInterface;
use React\Filesystem\Node;
use React\Filesystem\PollInterface;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use function React\Promise\all;

final class Directory implements Node\DirectoryInterface
{
use StatTrait;

private PollInterface $poll;
private AdapterInterface $filesystem;
private string $path;
private string $name;

public function __construct(PollInterface $poll, AdapterInterface $filesystem, string $path, string $name)
{
$this->poll = $poll;
$this->filesystem = $filesystem;
$this->path = $path;
$this->name = $name;
}

public function stat(): PromiseInterface
{
return $this->internalStat($this->path . $this->name);
}

public function ls(): PromiseInterface
{
$this->activate();
return new Promise(function (callable $resolve): void {
\eio_readdir($this->path . $this->name . DIRECTORY_SEPARATOR, \EIO_READDIR_STAT_ORDER | \EIO_READDIR_DIRS_FIRST, \EIO_PRI_DEFAULT, function ($_, $contents) use ($resolve): void {
$this->deactivate();
$list = [];
foreach ($contents['dents'] as $node) {
$fullPath = $this->path . $this->name . DIRECTORY_SEPARATOR . $node['name'];
switch ($node['type'] ?? null) {
case EIO_DT_DIR:
$list[] = $this->filesystem->directory($fullPath);
break;
case EIO_DT_REG :
$list[] = $this->filesystem->file($fullPath);
break;
default:
$list[] = $this->filesystem->detect($this->path . $this->name . DIRECTORY_SEPARATOR . $node['name']);
break;
}
}

$resolve(all($list));
});
});
}

public function unlink(): PromiseInterface
{
$this->activate();
return new Promise(function (callable $resolve): void {
\eio_readdir($this->path . $this->name . DIRECTORY_SEPARATOR, \EIO_READDIR_STAT_ORDER | \EIO_READDIR_DIRS_FIRST, \EIO_PRI_DEFAULT, function ($_, $contents) use ($resolve): void {
$this->deactivate();
if (count($contents['dents']) > 0) {
$resolve(false);

return;
}

$this->activate();
\eio_rmdir($this->path . $this->name, function () use ($resolve): void {
$this->deactivate();
$resolve(true);
});
});
});
}

public function path(): string
{
return $this->path;
}

public function name(): string
{
return $this->name;
}

protected function activate(): void
{
$this->poll->activate();
}

protected function deactivate(): void
{
$this->poll->deactivate();
}
}
25 changes: 25 additions & 0 deletions src/Eio/EventStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace React\Filesystem\Eio;

/**
* Singleton to make sure we always only have one file descriptor for the ext-eio event stream.
* Creating more than one will invalidate the previous ones and make anything still using those fail.
*
* @internal
*/
final class EventStream
{
private static $fd = null;

public static function get()
{
if (self::$fd !== null) {
return self::$fd;
}

self::$fd = eio_get_event_stream();

return self::$fd;
}
}
149 changes: 149 additions & 0 deletions src/Eio/File.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace React\Filesystem\Eio;

use React\Filesystem\Node\FileInterface;
use React\Filesystem\PollInterface;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use function React\Promise\resolve;

final class File implements FileInterface
{
use StatTrait;

private PollInterface $poll;
private string $path;
private string $name;

public function __construct(PollInterface $poll, string $path, string $name)
{
$this->poll = $poll;
$this->path = $path;
$this->name = $name;
}

public function stat(): PromiseInterface
{
return $this->internalStat($this->path . $this->name);
}

public function getContents(int $offset = 0 , ?int $maxlen = null): PromiseInterface
{
$this->activate();
return $this->openFile(
$this->path . DIRECTORY_SEPARATOR . $this->name,
\EIO_O_RDONLY,
0,
)->then(
function ($fileDescriptor) use ($offset, $maxlen): PromiseInterface {
if ($maxlen === null) {
$sizePromise = $this->statFileDescriptor($fileDescriptor)->then(static function ($stat): int {
return (int)$stat['size'];
});
} else {
$sizePromise = resolve($maxlen);
}
return $sizePromise->then(function ($length) use ($fileDescriptor, $offset): PromiseInterface {
return new Promise (function (callable $resolve) use ($fileDescriptor, $offset, $length): void {
\eio_read($fileDescriptor, $length, $offset, \EIO_PRI_DEFAULT, function ($fileDescriptor, string $buffer) use ($resolve): void {
$resolve($this->closeOpenFile($fileDescriptor)->then(function () use ($buffer): string {
return $buffer;
}));
}, $fileDescriptor);
});
});
}
);
}

public function putContents(string $contents, int $flags = 0)
{
$this->activate();
return $this->openFile(
$this->path . DIRECTORY_SEPARATOR . $this->name,
(($flags & \FILE_APPEND) == \FILE_APPEND) ? \EIO_O_RDWR | \EIO_O_APPEND : \EIO_O_RDWR | \EIO_O_CREAT,
0644
)->then(
function ($fileDescriptor) use ($contents, $flags): PromiseInterface {
return new Promise (function (callable $resolve) use ($contents, $fileDescriptor): void {
\eio_write($fileDescriptor, $contents, strlen($contents), 0, \EIO_PRI_DEFAULT, function ($fileDescriptor, int $bytesWritten) use ($resolve): void {
$resolve($this->closeOpenFile($fileDescriptor)->then(function () use ($bytesWritten): int {
return $bytesWritten;
}));
}, $fileDescriptor);
});
}
);
}

private function statFileDescriptor($fileDescriptor): PromiseInterface
{
return new Promise(function (callable $resolve, callable $reject) use ($fileDescriptor) {
\eio_fstat($fileDescriptor, \EIO_PRI_DEFAULT, function ($_, $stat) use ($resolve): void {
$resolve($stat);
}, $fileDescriptor);
});
}

private function openFile(string $path, int $flags, int $mode): PromiseInterface
{
return new Promise(function (callable $resolve, callable $reject) use ($path, $flags, $mode): void {
\eio_open(
$path,
$flags,
$mode,
\EIO_PRI_DEFAULT,
function ($_, $fileDescriptor) use ($resolve): void {
$resolve($fileDescriptor);
}
);
});
}

private function closeOpenFile($fileDescriptor): PromiseInterface
{
return new Promise(function (callable $resolve) use ($fileDescriptor) {
try {
\eio_close($fileDescriptor, \EIO_PRI_DEFAULT, function () use ($resolve): void {
$this->deactivate();
$resolve();
});
} catch (\Throwable $error) {
$this->deactivate();
throw $error;
}
});
}

public function unlink(): PromiseInterface
{
$this->activate();
return new Promise(function (callable $resolve): void {
\eio_unlink($this->path . DIRECTORY_SEPARATOR . $this->name, \EIO_PRI_DEFAULT, function () use ($resolve): void {
$this->deactivate();
$resolve(true);
});
});
}

public function path(): string
{
return $this->path;
}

public function name(): string
{
return $this->name;
}

protected function activate(): void
{
$this->poll->activate();
}

protected function deactivate(): void
{
$this->poll->deactivate();
}
}

0 comments on commit ca5e06f

Please sign in to comment.