- allow to avoid deps on shared extensions on build time
[m6w6/ext-http] / lib / BigGet.php
1 <?php
2
3 /**
4 * BigGet - download big files efficiently
5 * $Id$
6 *
7 * @copyright Michael Wallner, <mike@iworks.at>
8 * @license BSD, revised
9 * @version $Revision$
10 */
11 class BigGet extends HttpRequestPool
12 {
13 /**
14 * File split size
15 */
16 const SIZE = 1048576;
17
18 /**
19 * Parallel Request count
20 */
21 const RMAX = 5;
22
23 /**
24 * Whether to output debug messages
25 *
26 * @var bool
27 */
28 public $dbg = false;
29
30 /**
31 * URL
32 *
33 * @var string
34 */
35 private $url;
36
37 /**
38 * Temp file prefix
39 *
40 * @var string
41 */
42 private $tmp;
43
44 /**
45 * Size of requested resource
46 *
47 * @var int
48 */
49 private $size;
50
51 /**
52 * Whether the requests have been sent
53 *
54 * @var bool
55 */
56 private $sent = false;
57
58 /**
59 * Request counter
60 *
61 * @var int
62 */
63 private $count = 0;
64
65 /**
66 * Static constructor
67 *
68 * @param string $url
69 * @param string $tmp
70 * @return BigGet
71 * @throws Exception
72 */
73 public static function url($url, $tmp = '/tmp')
74 {
75 $head = new HttpRequest($url, HttpRequest::METH_HEAD);
76 $headers = $head->send()->getHeaders();
77
78 if (200 != $head->getResponseCode()) {
79 throw new HttpException("Did not receive '200 Ok' from HEAD $url");
80 }
81 if (!isset($headers['Accept-Ranges'])) {
82 throw new HttpException("Did not receive an Accept-Ranges header from HEAD $url");
83 }
84 if (!isset($headers['Content-Length'])) {
85 throw new HttpException("Did not receive a Content-Length header from HEAD $url");
86 }
87
88 $bigget = new BigGet;
89 $bigget->url = $url;
90 $bigget->tmp = tempnam($tmp, 'BigGet.');
91 $bigget->size = $headers['Content-Length'];
92 return $bigget;
93 }
94
95 /**
96 * Save the resource to a file
97 *
98 * @param string $file
99 * @return bool
100 * @throws Exception
101 */
102 public function saveTo($file)
103 {
104 $this->sent or $this->send();
105
106 if ($w = fopen($this->tmp, 'wb')) {
107
108 $this->dbg && print "\nCopying temp files to $file ...\n";
109
110 foreach (glob($this->tmp .".????") as $tmp) {
111
112 $this->dbg && print "\t$tmp\n";
113
114 if ($r = fopen($tmp, 'rb')) {
115 stream_copy_to_stream($r, $w);
116 fclose($r);
117 }
118 unlink($tmp);
119 }
120 fclose($w);
121 rename($this->tmp, $file);
122
123 $this->dbg && print "\nDone.\n";
124
125 return true;
126 }
127 return false;
128 }
129
130 /**
131 * Overrides HttpRequestPool::send()
132 *
133 * @return void
134 * @throws Exception
135 */
136 public function send()
137 {
138 $this->sent = true;
139
140 // use max RMAX simultanous requests with a req size of SIZE
141 while ($this->count < self::RMAX && -1 != $offset = $this->getRangeOffset()) {
142 $this->attachNew($offset);
143 }
144
145 while ($this->socketPerform()) {
146 if (!$this->socketSelect()) {
147 throw new HttpSocketException;
148 }
149 }
150 }
151
152 /**
153 * Overrides HttpRequestPool::socketPerform()
154 *
155 * @return bool
156 */
157 protected function socketPerform()
158 {
159 try {
160 $rs = parent::socketPerform();
161 } catch (HttpRequestPoolException $x) {
162 foreach ($x->exceptionStack as $e) {
163 echo $e->getMessage(), "\n";
164 }
165 }
166
167 foreach ($this->getFinishedRequests() as $r) {
168 $this->detach($r);
169
170 if (206 != $rc = $r->getResponseCode()) {
171 throw new HttpException("Unexpected response code: $rc");
172 }
173
174 file_put_contents(sprintf("%s.%04d", $this->tmp, $r->id), $r->getResponseBody());
175
176 if (-1 != $offset = $this->getRangeOffset()) {
177 $this->attachNew($offset);
178 }
179 }
180
181 return $rs;
182 }
183
184 private function attachNew($offset)
185 {
186 $stop = min($this->count * self::SIZE + self::SIZE, $this->size) - 1;
187
188 $this->dbg && print "Attaching new request to get range: $offset-$stop\n";
189
190 $req = new BigGetRequest(
191 $this->url,
192 HttpRequest::METH_GET,
193 array(
194 'headers' => array(
195 'Range' => "bytes=$offset-$stop"
196 )
197 )
198 );
199 $this->attach($req);
200 $req->id = $this->count++;
201 }
202
203 private function getRangeOffset()
204 {
205 return ($this->size >= $start = $this->count * self::SIZE) ? $start : -1;
206 }
207 }
208
209
210 /**
211 * BigGet request
212 * @ignore
213 */
214 class BigGetRequest extends HttpRequest
215 {
216 public $id;
217 }
218
219 ?>