diff --git a/src/Eio/Adapter.php b/src/Eio/Adapter.php index 8b244101..09c6e241 100644 --- a/src/Eio/Adapter.php +++ b/src/Eio/Adapter.php @@ -21,7 +21,7 @@ final class Adapter implements AdapterInterface public function __construct() { $this->loop = Loop::get(); - $this->poll = new Poll($this->loop); + $this->poll = new Poll(); } public function detect(string $path): PromiseInterface diff --git a/src/Eio/Directory.php b/src/Eio/Directory.php index a5ee925b..160f9b85 100644 --- a/src/Eio/Directory.php +++ b/src/Eio/Directory.php @@ -34,10 +34,15 @@ public function stat(): PromiseInterface 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 { + return new Promise(function (callable $resolve, callable $reject): void { + \eio_readdir($this->path . $this->name . DIRECTORY_SEPARATOR, \EIO_READDIR_STAT_ORDER | \EIO_READDIR_DIRS_FIRST, \EIO_PRI_DEFAULT, function ($_, $contents, $resource) use ($resolve, $reject): void { $this->deactivate(); $list = []; + if ($contents === -1) { + $reject(new \RuntimeException('Error reading from directory "' . $this->path . $this->name . DIRECTORY_SEPARATOR . '": ' . \eio_get_last_error($resource))); + return; + } + foreach ($contents['dents'] as $node) { $fullPath = $this->path . $this->name . DIRECTORY_SEPARATOR . $node['name']; switch ($node['type'] ?? null) { diff --git a/src/Eio/File.php b/src/Eio/File.php index c4c7d638..6c8e07b0 100644 --- a/src/Eio/File.php +++ b/src/Eio/File.php @@ -10,6 +10,9 @@ final class File implements FileInterface { + private const READ_CHUNK_FIZE = 65536; +// private const READ_CHUNK_FIZE = 1; + use StatTrait; private PollInterface $poll; @@ -37,22 +40,37 @@ public function getContents(int $offset = 0 , ?int $maxlen = null): PromiseInter 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); + $buffer = ''; + $bufferLength = 0; + $read = function (bool $finalAttempt, int $offset) use ($fileDescriptor, $maxlen, &$read, &$buffer, &$bufferLength): PromiseInterface { + return new Promise (function (callable $resolve) use ($fileDescriptor, $offset, $maxlen, $finalAttempt, &$read, &$buffer, &$bufferLength): void { + \eio_read($fileDescriptor, $maxlen ?? self::READ_CHUNK_FIZE, $offset, \PHP_INT_MAX, function ($fileDescriptor, string $contents) use ($resolve, $maxlen, $finalAttempt, &$read, &$buffer, &$bufferLength): void { + $contentLength = strlen($contents); + $buffer .= $contents; + $bufferLength += $contentLength; + + if ( + ($maxlen === null && $finalAttempt) || + ($maxlen !== null && $bufferLength >= $maxlen) + ) { + if ($maxlen !== null && $bufferLength > $maxlen) { + $buffer = substr($buffer, 0, $maxlen); + } + + $resolve($this->closeOpenFile($fileDescriptor)->then(function () use ($buffer): string { + $this->deactivate(); + return $buffer; + })); + } else if ($maxlen === null && !$finalAttempt && $contentLength === 0) { + $resolve($read(true, $bufferLength)); + } else { + $resolve($read(false, $bufferLength)); + } + }); }); - }); + }; + + return $read(false, $offset); } ); } @@ -61,9 +79,9 @@ 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 + $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 { @@ -77,15 +95,6 @@ function ($fileDescriptor) use ($contents, $flags): PromiseInterface { ); } - 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 { diff --git a/src/Eio/Poll.php b/src/Eio/Poll.php index ff97e8f4..fb07d9f6 100644 --- a/src/Eio/Poll.php +++ b/src/Eio/Poll.php @@ -2,20 +2,21 @@ namespace React\Filesystem\Eio; -use React\EventLoop\LoopInterface; +use React\EventLoop\Loop; use React\Filesystem\PollInterface; +/** + * @internal + */ final class Poll implements PollInterface { - private LoopInterface $loop; private $fd; private \Closure $handleEvent; private int $workInProgress = 0; - public function __construct(LoopInterface $loop) + public function __construct() { $this->fd = EventStream::get(); - $this->loop = $loop; $this->handleEvent = function () { $this->handleEvent(); }; @@ -24,7 +25,7 @@ public function __construct(LoopInterface $loop) public function activate(): void { if ($this->workInProgress++ === 0) { - $this->loop->addReadStream($this->fd, $this->handleEvent); + Loop::addReadStream($this->fd, $this->handleEvent); } } @@ -38,7 +39,7 @@ private function handleEvent() public function deactivate(): void { if (--$this->workInProgress <= 0) { - $this->loop->removeReadStream($this->fd); + Loop::removeReadStream($this->fd); } } } diff --git a/src/Fallback/File.php b/src/Fallback/File.php index 25c26e7c..803f310a 100644 --- a/src/Fallback/File.php +++ b/src/Fallback/File.php @@ -29,7 +29,7 @@ public function stat(): PromiseInterface public function getContents(int $offset = 0 , ?int $maxlen = null): PromiseInterface { $path = $this->path . $this->name; - return resolve(file_get_contents($path, false, null, $offset, $maxlen ?? (int)stat($path)['size'])); + return resolve(file_get_contents($path, false, null, $offset, $maxlen)); } public function putContents(string $contents, int $flags = 0): PromiseInterface diff --git a/src/Uv/File.php b/src/Uv/File.php index f423ef9b..423f8208 100644 --- a/src/Uv/File.php +++ b/src/Uv/File.php @@ -3,6 +3,7 @@ namespace React\Filesystem\Uv; use React\EventLoop\ExtUvLoop; +use React\EventLoop\Loop; use React\Filesystem\Node\FileInterface; use React\Filesystem\PollInterface; use React\Promise\Promise; @@ -11,6 +12,9 @@ final class File implements FileInterface { +// private const READ_CHUNK_FIZE = 65536; + private const READ_CHUNK_FIZE = 1; + use StatTrait; private ExtUvLoop $loop; @@ -36,41 +40,102 @@ public function stat(): PromiseInterface public function getContents(int $offset = 0 , ?int $maxlen = null): PromiseInterface { $this->activate(); - return new Promise(function (callable $resolve) use ($offset, $maxlen): void { - uv_fs_open($this->uvLoop, $this->path . DIRECTORY_SEPARATOR . $this->name, UV::O_RDONLY, 0, function ($fileDescriptor) use ($resolve, $offset, $maxlen): void { - uv_fs_fstat($this->uvLoop, $fileDescriptor, function ($fileDescriptor, array $stat) use ($resolve, $offset, $maxlen): void { - uv_fs_read($this->uvLoop, $fileDescriptor, $offset, $maxlen ?? (int)$stat['size'], function ($fileDescriptor, string $buffer) use ($resolve): void { - $resolve($buffer); - uv_fs_close($this->uvLoop, $fileDescriptor, function () { - $this->deactivate(); + return $this->openFile( + $this->path . DIRECTORY_SEPARATOR . $this->name, + UV::O_RDONLY, + 0, + )->then( + function ($fileDescriptor) use ($offset, $maxlen): PromiseInterface { + $buffer = ''; + $bufferLength = 0; + $read = function (bool $finalAttempt, int $offset) use ($fileDescriptor, $maxlen, &$read, &$buffer, &$bufferLength): PromiseInterface { + return new Promise (function (callable $resolve) use ($fileDescriptor, $offset, $maxlen, $finalAttempt, &$read, &$buffer, &$bufferLength): void { + \uv_fs_read($this->uvLoop, $fileDescriptor, $offset, $maxlen ?? self::READ_CHUNK_FIZE, function ($fileDescriptor, string $contents) use ($resolve, $maxlen, $finalAttempt, &$read, &$buffer, &$bufferLength): void { + $contentLength = strlen($contents); + $buffer .= $contents; + $bufferLength += $contentLength; + + if ( + ($maxlen === null && $finalAttempt) || + ($maxlen !== null && $bufferLength >= $maxlen) + ) { + if ($maxlen !== null && $bufferLength > $maxlen) { + $buffer = substr($buffer, 0, $maxlen); + } + + $resolve($this->closeOpenFile($fileDescriptor)->then(function () use ($buffer): string { + $this->deactivate(); + + return $buffer; + })); + } else if ($maxlen === null && !$finalAttempt && $contentLength === 0) { + $resolve($read(true, $bufferLength)); + } else { + $resolve($read(false, $bufferLength)); + } }); }); - }); - }); - }); + }; + + return $read(false, $offset); + } + ); } public function putContents(string $contents, int $flags = 0) { $this->activate(); - return new Promise(function (callable $resolve) use ($contents, $flags): void { - uv_fs_open( - $this->uvLoop, - $this->path . DIRECTORY_SEPARATOR . $this->name, - (($flags & \FILE_APPEND) == \FILE_APPEND) ? UV::O_RDWR | UV::O_CREAT | UV::O_APPEND : UV::O_RDWR | UV::O_CREAT, - 0644, - function ($fileDescriptor) use ($resolve, $contents, $flags): void { + return $this->openFile( + $this->path . DIRECTORY_SEPARATOR . $this->name, + (($flags & \FILE_APPEND) == \FILE_APPEND) ? UV::O_RDWR | UV::O_CREAT | UV::O_APPEND : UV::O_RDWR | UV::O_CREAT, + 0644, + )->then( + function ($fileDescriptor) use ($contents): PromiseInterface { + return new Promise (function (callable $resolve) use ($contents, $fileDescriptor): void { uv_fs_write($this->uvLoop, $fileDescriptor, $contents, 0, function ($fileDescriptor, int $bytesWritten) use ($resolve): void { - $resolve($bytesWritten); - uv_fs_close($this->uvLoop, $fileDescriptor, function () { + $resolve($this->closeOpenFile($fileDescriptor)->then(function () use ($bytesWritten): int { $this->deactivate(); - }); + return $bytesWritten; + })); }); } ); }); } + private function openFile(string $path, int $flags, int $mode): PromiseInterface + { + $this->activate(); + return new Promise(function (callable $resolve) use ($path, $flags, $mode): void { + uv_fs_open( + $this->uvLoop, + $this->path . DIRECTORY_SEPARATOR . $this->name, + $flags, + $mode, + function ($fileDescriptor) use ($resolve): void { + $this->deactivate(); + $resolve($fileDescriptor); + } + ); + }); + } + + private function closeOpenFile($fileDescriptor): PromiseInterface + { + $this->activate(); + return new Promise(function (callable $resolve) use ($fileDescriptor) { + try { + uv_fs_close($this->uvLoop, $fileDescriptor, function () use ($resolve) { + $this->deactivate(); + $resolve(); + }); + } catch (\Throwable $error) { + $this->deactivate(); + throw $error; + } + }); + } + public function unlink(): PromiseInterface { $this->activate(); diff --git a/tests/AbstractFilesystemTestCase.php b/tests/AbstractFilesystemTestCase.php index cdbc396e..11347a62 100644 --- a/tests/AbstractFilesystemTestCase.php +++ b/tests/AbstractFilesystemTestCase.php @@ -31,6 +31,6 @@ final public function provideFilesystems(): iterable yield 'uv' => [new Uv\Adapter()]; } - yield 'factory' => [Factory::create()]; +// yield 'factory' => [Factory::create()]; } } diff --git a/tests/DirectoryTest.php b/tests/DirectoryTest.php index 575fd2c8..7552f0c0 100644 --- a/tests/DirectoryTest.php +++ b/tests/DirectoryTest.php @@ -2,7 +2,6 @@ namespace React\Tests\Filesystem; -use React\EventLoop\LoopInterface; use React\Filesystem\AdapterInterface; use React\Filesystem\Node\DirectoryInterface; use React\Filesystem\Node\FileInterface; diff --git a/tests/FileTest.php b/tests/FileTest.php index e443a062..2fdab993 100644 --- a/tests/FileTest.php +++ b/tests/FileTest.php @@ -35,9 +35,26 @@ public function stat(AdapterInterface $filesystem): void */ public function getContents(AdapterInterface $filesystem): void { + $blockingReadFileContents = file_get_contents(__FILE__); $fileContents = await($filesystem->detect(__FILE__)->then(static function (FileInterface $file): PromiseInterface { return $file->getContents(); })); + file_put_contents(__FILE__ . '.overflow', $fileContents); + +// self::assertSame(strlen($blockingReadFileContents), strlen($fileContents)); + self::assertSame($blockingReadFileContents, $fileContents); + } + + /** + * @test + * + * @dataProvider provideFilesystems + */ + public function getContentsWithFilesize(AdapterInterface $filesystem): void + { + $fileContents = await($filesystem->detect(__FILE__)->then(static function (FileInterface $file): PromiseInterface { + return $file->getContents(0, filesize(__FILE__)); + })); self::assertSame(file_get_contents(__FILE__), $fileContents); } @@ -95,7 +112,7 @@ public function putContentsMultipleBigFiles(AdapterInterface $filesystem): void $fileNames[] = $directoryName . bin2hex(random_bytes(13)); } foreach ($fileNames as $fileName) { - $fileContents[$fileName] = bin2hex(random_bytes(4096)); + $fileContents[$fileName] = bin2hex(random_bytes(4194304)); touch($fileName); } @@ -155,7 +172,7 @@ public function putContentsAppendBigFile(AdapterInterface $filesystem): void $fileContents = []; $writtenLength = 0; for ($i = 0; $i < 13; $i++) { - $fileContents[] = bin2hex(random_bytes(4096)); + $fileContents[] = bin2hex(random_bytes(4194304)); } foreach ($fileContents as $fileContent) { @@ -177,7 +194,7 @@ public function putContentsAppendBigFile(AdapterInterface $filesystem): void */ public function putContentsAppendMultipleBigFiles(AdapterInterface $filesystem): void { - $this->runMultipleFilesTests($filesystem, 8, 4096, 4); + $this->runMultipleFilesTests($filesystem, 8, 4194304, 4); } /** @@ -187,7 +204,7 @@ public function putContentsAppendMultipleBigFiles(AdapterInterface $filesystem): */ public function putContentsAppendLotsOfSmallFiles(AdapterInterface $filesystem): void { - $this->runMultipleFilesTests($filesystem, 16, 16, 4); + $this->runMultipleFilesTests($filesystem, 16, 16384, 4); } /** @@ -197,7 +214,7 @@ public function putContentsAppendLotsOfSmallFiles(AdapterInterface $filesystem): */ public function putContentsAppendLoadsOfSmallFiles(AdapterInterface $filesystem): void { - $this->runMultipleFilesTests($filesystem, 32, 8, 8); + $this->runMultipleFilesTests($filesystem, 32, 8192, 8); } public function runMultipleFilesTests(AdapterInterface $filesystem, int $fileCount, int $fileSize, int $chunkCount): void