Skip to content

Commit 357cf39

Browse files
committed
Merge pull request #20 from clue-labs/stream
Add streaming API for fetching larger files (fetchFileStream() method)
2 parents ff1ff3c + 4ab3581 commit 357cf39

File tree

6 files changed

+120
-2
lines changed

6 files changed

+120
-2
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,40 @@ try {
111111

112112
Refer to [clue/block-react](https://github.com/clue/php-block-react#readme) for more details.
113113

114+
#### Streaming
115+
116+
The following API endpoint resolves with the file contents as a string:
117+
118+
```php
119+
$client->fetchFile($path);
120+
````
121+
122+
Keep in mind that this means the whole string has to be kept in memory.
123+
This is easy to get started and works reasonably well for smaller files.
124+
125+
For bigger files it's usually a better idea to use a streaming approach,
126+
where only small chunks have to be kept in memory.
127+
This works for (any number of) files of arbitrary sizes.
128+
129+
The following API endpoint complements the default Promise-based API and returns
130+
an instance implementing `ReadableStreamInterface` instead:
131+
132+
```php
133+
$stream = $client->fetchFileStream($path);
134+
135+
$stream->on('data', function ($chunk) {
136+
echo $chunk;
137+
});
138+
139+
$stream->on('error', function (Exception $error) {
140+
echo 'Error: ' . $error->getMessage() . PHP_EOL;
141+
});
142+
143+
$stream->on('close', function () {
144+
echo '[DONE]' . PHP_EOL;
145+
});
146+
```
147+
114148
## Install
115149

116150
The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md)

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"clue/buzz-react": "^0.5",
2121
"ext-simplexml": "*",
2222
"neitanod/forceutf8": "~1.4",
23-
"rize/uri-template": "^0.3"
23+
"rize/uri-template": "^0.3",
24+
"clue/promise-stream-react": "^0.1"
2425
},
2526
"require-dev": {
2627
"clue/block-react": "~0.3.0"

examples/directory.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use Clue\React\ViewVcApi\Client;
44
use React\EventLoop\Factory as LoopFactory;
55
use Clue\React\Buzz\Browser;
6+
use React\Stream\Stream;
67

78
require __DIR__ . '/../vendor/autoload.php';
89

@@ -31,7 +32,17 @@
3132
if (substr($path, -1) === '/') {
3233
$client->fetchDirectory($path, $revision)->then('print_r', 'printf');
3334
} else {
34-
$client->fetchFile($path, $revision)->then('print_r', 'printf');
35+
//$client->fetchFile($path, $revision)->then('print_r', 'printf');
36+
37+
$stream = $client->fetchFileStream($path, $revision);
38+
39+
// any errors
40+
$stream->on('error', 'printf');
41+
42+
// pipe stream into STDOUT
43+
$out = new Stream(STDOUT, $loop);
44+
$out->pause();
45+
$stream->pipe($out);
3546
}
3647

3748
$loop->run();

src/Client.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Clue\React\ViewVcApi\Io\Parser;
1313
use Clue\React\ViewVcApi\Io\Loader;
1414
use Rize\UriTemplate;
15+
use Clue\React\Promise\Stream;
1516

1617
class Client
1718
{
@@ -65,6 +66,46 @@ public function fetchFile($path, $revision = null)
6566
);
6667
}
6768

69+
/**
70+
* Reads the file contents of the given file path as a readable stream
71+
*
72+
* This works for files of arbitrary sizes as only small chunks have to
73+
* be kept in memory. The resulting stream is a well-behaving readable stream
74+
* that will emit the normal stream events.
75+
*
76+
* @param string $path
77+
* @param string|null $revision
78+
* @return ReadableStreamInterface
79+
* @throws InvalidArgumentException
80+
* @see self::fetchFile()
81+
*/
82+
public function fetchFileStream($path, $revision = null)
83+
{
84+
if (substr($path, -1) === '/') {
85+
throw new InvalidArgumentException('File path MUST NOT end with trailing slash');
86+
}
87+
88+
// TODO: fetching a directory redirects to path with trailing slash
89+
// TODO: status returns 200 OK, but displays an error message anyways..
90+
// TODO: see not-a-file.html
91+
// TODO: reject all paths with trailing slashes
92+
93+
return Stream\unwrapReadable(
94+
$this->browser->withOptions(array('streaming' => true))->get(
95+
$this->uri->expand(
96+
'{+path}?view=co{&pathrev}',
97+
array(
98+
'path' => $path,
99+
'pathrev' => $revision
100+
)
101+
)
102+
)->then(function (ResponseInterface $response) {
103+
// the body implements ReadableStreamInterface, so let's just return this to the unwrapper
104+
return $response->getBody();
105+
})
106+
);
107+
}
108+
68109
public function fetchDirectory($path, $revision = null, $showAttic = false)
69110
{
70111
if (substr($path, -1) !== '/') {

tests/ClientTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ public function testFetchFile()
4444
$this->expectPromiseResolveWith('# hello', $promise);
4545
}
4646

47+
public function testFetchFileStream()
48+
{
49+
$response = new Response(200, array(), '# hello', '1.0', 'OK');
50+
51+
$this->expectRequest($this->uri . 'README.md?view=co')->will($this->returnValue(Promise\reject()));
52+
53+
$stream = $this->client->fetchFileStream('README.md');
54+
55+
$this->assertInstanceOf('React\Stream\ReadableStreamInterface', $stream);
56+
}
57+
58+
/**
59+
* @expectedException InvalidArgumentException
60+
*/
61+
public function testInvalidFileStream()
62+
{
63+
$this->client->fetchFileStream('invalid/');
64+
}
65+
4766
public function testFetchFileExcessiveSlashesAreIgnored()
4867
{
4968
$this->expectRequest($this->uri . 'README.md?view=co')->will($this->returnValue(Promise\reject()));

tests/FunctionalApacheClientTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use React\EventLoop\Factory as LoopFactory;
55
use Clue\React\Buzz\Browser;
66
use Clue\React\Block;
7+
use Clue\React\Promise\Stream;
78

89
class FunctionalApacheClientTest extends TestCase
910
{
@@ -41,6 +42,17 @@ public function testFetchFile()
4142
$this->assertStringStartsWith('/*', $recipe);
4243
}
4344

45+
public function testFetchFileStream()
46+
{
47+
$file = 'jakarta/ecs/tags/V1_0/src/java/org/apache/ecs/AlignType.java';
48+
$revision = '168703';
49+
50+
$promise = $this->viewvc->fetchFileStream($file, $revision);
51+
$recipe = Block\await(Stream\buffer($promise), $this->loop);
52+
53+
$this->assertStringStartsWith('/*', $recipe);
54+
}
55+
4456
public function testFetchFileOldFileNowDeletedButRevisionAvailable()
4557
{
4658
$file = 'commons/STATUS';

0 commit comments

Comments
 (0)