Skip to content

Commit 8e651cf

Browse files
committed
Implement a way to stream a raw file upload
This will be helpful when receiving a very large file upload, and only when the request body is the raw file content.
1 parent 23441cf commit 8e651cf

File tree

2 files changed

+231
-1
lines changed

2 files changed

+231
-1
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
],
1515
"require": {
1616
"php": "^7.1.10",
17-
"ext-curl": "*"
17+
"ext-curl": "*",
18+
"elementaryframework/events": "^0.0.1",
19+
"elementaryframework/streams": "^0.0.2"
1820
},
1921
"keywords": [
2022
"url",
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<?php
2+
3+
/**
4+
* WaterPipe - URL routing framework for PHP
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*
24+
* @category Library
25+
* @package WaterPipe
26+
* @author Axel Nana <[email protected]>
27+
* @copyright 2018 Aliens Group, Inc.
28+
* @license MIT <https://github.com/ElementaryFramework/WaterPipe/blob/master/LICENSE>
29+
* @version 1.3.0
30+
* @link http://waterpipe.na2axl.tk
31+
*/
32+
33+
namespace ElementaryFramework\WaterPipe\Streams;
34+
35+
use Exception;
36+
use Closure;
37+
38+
use ElementaryFramework\Core\Events\WithEvents;
39+
use ElementaryFramework\Core\Streams\Events\StreamEvent;
40+
use ElementaryFramework\Core\Streams\IReadableStream;
41+
use ElementaryFramework\Core\Streams\IWritableStream;
42+
use ElementaryFramework\WaterPipe\HTTP\Request\Request;
43+
44+
/**
45+
* A Stream used to handle large raw file uploads.
46+
*/
47+
class RawFileUploadStream implements IReadableStream
48+
{
49+
use WithEvents;
50+
51+
private $_closed;
52+
53+
private $_paused;
54+
55+
private $_handle;
56+
57+
private $_receiving;
58+
59+
private $_destinationHandle;
60+
61+
/**
62+
* Creates a new {@link FileUploadStream} from the given request to the given destination.
63+
*
64+
* @param Request $request The request from which read the uploaded file content.
65+
* @param string $destination The destination file path in which write the content read from request.
66+
* @param bool $append Define if the destination file will be overwritten or appended with new content.
67+
*/
68+
public function __construct(Request $request, string $destination = null, bool $append = false)
69+
{
70+
if ($destination !== null) {
71+
$this->_destinationHandle = fopen($destination, $append ? "a" : "w");
72+
}
73+
74+
$this->_handle = fopen('php://input', "r");
75+
76+
$this->_closed = false;
77+
$this->_paused = false;
78+
79+
$this->on(
80+
StreamEvent::EVENT_END,
81+
Closure::bind(function () {
82+
$this->_receiving = false;
83+
}, $this)
84+
);
85+
86+
$this->on(
87+
StreamEvent::EVENT_DATA,
88+
Closure::bind(function ($data) {
89+
if ($this->_destinationHandle != null) {
90+
fwrite($this->_destinationHandle, $data);
91+
}
92+
}, $this)
93+
);
94+
95+
$this->on(
96+
StreamEvent::EVENT_CLOSE,
97+
Closure::bind(function () {
98+
if ($this->_destinationHandle != null) {
99+
fclose($this->_destinationHandle);
100+
}
101+
}, $this)
102+
);
103+
}
104+
105+
/**
106+
* @inheritDoc
107+
*/
108+
public function close(): void
109+
{
110+
if ($this->_closed)
111+
return;
112+
113+
if ($this->_closed = fclose($this->_handle)) {
114+
$this->emit(StreamEvent::EVENT_CLOSE);
115+
} else {
116+
$this->emit(
117+
StreamEvent::EVENT_ERROR,
118+
new Exception("Unable to close the upload stream.")
119+
);
120+
}
121+
}
122+
123+
/**
124+
* @inheritDoc
125+
*/
126+
public function isReadable(): bool
127+
{
128+
return !$this->_closed && !feof($this->_handle);
129+
}
130+
131+
/**
132+
* @inheritDoc
133+
*/
134+
public function pause(): void
135+
{
136+
if ($this->_paused)
137+
return;
138+
139+
$this->_paused = true;
140+
}
141+
142+
/**
143+
* @inheritDoc
144+
*/
145+
public function resume(): void
146+
{
147+
if (!$this->_paused)
148+
return;
149+
150+
$this->_paused = false;
151+
}
152+
153+
/**
154+
* @inheritDoc
155+
*/
156+
public function pipe(IWritableStream $destination, bool $autoEnd = true): IReadableStream
157+
{
158+
if (!$this->isReadable())
159+
return null;
160+
161+
if (!$destination->isWritable())
162+
$this->pause();
163+
164+
if ($autoEnd) {
165+
$this->on(
166+
StreamEvent::EVENT_END,
167+
function ($data) use (&$destination) {
168+
$destination->end($data);
169+
}
170+
);
171+
}
172+
173+
$destination->emit(
174+
StreamEvent::EVENT_PIPE,
175+
$this
176+
);
177+
178+
return $this;
179+
}
180+
181+
/**
182+
* @inheritDoc
183+
*/
184+
public function read(int $length): void
185+
{
186+
if ($this->isReadable()) {
187+
$data = fread($this->_handle, $length);
188+
189+
if ($data === false) {
190+
$this->emit(
191+
StreamEvent::EVENT_ERROR,
192+
new Exception("Unable to read the upload stream.")
193+
);
194+
} else {
195+
$this->emit(
196+
StreamEvent::EVENT_DATA,
197+
$data
198+
);
199+
}
200+
} else if (feof($this->_handle) && !$this->_closed) {
201+
$this->emit(StreamEvent::EVENT_END);
202+
}
203+
}
204+
205+
/**
206+
* Start receiving data from request.
207+
*
208+
* @return void
209+
*/
210+
public function receive()
211+
{
212+
$this->_receiving = true;
213+
214+
while ($this->_receiving) {
215+
$this->read(1024);
216+
}
217+
218+
$this->close();
219+
}
220+
221+
/**
222+
* Closes the stream when the instance is being destructed.
223+
*/
224+
function __destruct()
225+
{
226+
$this->close();
227+
}
228+
}

0 commit comments

Comments
 (0)