1
|
|
|
<?php |
2
|
|
|
namespace Tartana\Host\Common; |
3
|
|
|
|
4
|
|
|
use GuzzleHttp\Client; |
5
|
|
|
use GuzzleHttp\ClientInterface; |
6
|
|
|
use GuzzleHttp\Cookie\FileCookieJar; |
7
|
|
|
use GuzzleHttp\Exception\RequestException; |
8
|
|
|
use GuzzleHttp\Psr7\Request; |
9
|
|
|
use GuzzleHttp\Psr7\Response; |
10
|
|
|
use GuzzleHttp\RequestOptions; |
11
|
|
|
use Joomla\Registry\Registry; |
12
|
|
|
use League\Flysystem\Adapter\Local; |
13
|
|
|
use League\Flysystem\Config; |
14
|
|
|
use Tartana\Domain\Command\SaveDownloads; |
15
|
|
|
use Tartana\Entity\Download; |
16
|
|
|
use Tartana\Host\HostInterface; |
17
|
|
|
use Tartana\Mixins\CommandBusAwareTrait; |
18
|
|
|
use Tartana\Mixins\LoggerAwareTrait; |
19
|
|
|
|
20
|
|
|
class Http implements HostInterface |
21
|
|
|
{ |
22
|
|
|
use LoggerAwareTrait; |
23
|
|
|
use CommandBusAwareTrait; |
24
|
|
|
|
25
|
|
|
private $configuration = null; |
26
|
|
|
|
27
|
|
|
private $client = null; |
28
|
|
|
|
29
|
77 |
|
public function __construct(Registry $configuration, ClientInterface $client = null) |
30
|
|
|
{ |
31
|
77 |
|
$this->configuration = $configuration; |
32
|
77 |
|
$this->setClient($client); |
33
|
77 |
|
} |
34
|
|
|
|
35
|
3 |
|
public function fetchLinkList($link) |
36
|
|
|
{ |
37
|
|
|
return [ |
38
|
3 |
|
$link |
39
|
|
|
]; |
40
|
|
|
} |
41
|
|
|
|
42
|
9 |
|
public function fetchDownloadInfo(array $downloads) |
43
|
|
|
{ |
44
|
9 |
|
foreach ($downloads as $download) { |
45
|
|
|
// Connection check |
46
|
|
|
try { |
47
|
9 |
|
$originalName = $this->parseFileName($this->getClient() |
48
|
9 |
|
->head($download->getLink())); |
49
|
7 |
|
if (empty($originalName)) { |
50
|
4 |
|
$originalName = basename($download->getLink()); |
51
|
|
|
} |
52
|
7 |
|
if (!empty($originalName) && empty($download->getFileName())) { |
53
|
7 |
|
$download->setFileName($originalName); |
54
|
|
|
} |
55
|
2 |
|
} catch (\Exception $e) { |
56
|
2 |
|
$this->log('Exception fetching head for connection test: ' . $e->getMessage()); |
57
|
2 |
|
$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL'); |
58
|
9 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
59
|
|
|
} |
60
|
|
|
} |
61
|
9 |
|
} |
62
|
|
|
|
63
|
46 |
|
public function download(array $downloads) |
64
|
|
|
{ |
65
|
46 |
|
if (empty($downloads)) { |
66
|
2 |
|
return []; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
try { |
70
|
44 |
|
if (!$this->login()) { |
|
|
|
|
71
|
5 |
|
foreach ($downloads as $download) { |
72
|
5 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
73
|
5 |
|
$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_LOGIN'); |
74
|
|
|
} |
75
|
5 |
|
$this->handleCommand(new SaveDownloads($downloads)); |
76
|
42 |
|
return []; |
77
|
|
|
} |
78
|
2 |
|
} catch (\Exception $e) { |
79
|
2 |
|
foreach ($downloads as $download) { |
80
|
2 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
81
|
2 |
|
$download->setMessage($e->getMessage()); |
82
|
|
|
} |
83
|
2 |
|
$this->handleCommand(new SaveDownloads($downloads)); |
84
|
2 |
|
return []; |
85
|
|
|
} |
86
|
|
|
|
87
|
37 |
|
$promises = []; |
88
|
37 |
|
foreach ($downloads as $download) { |
89
|
37 |
|
$download->setStartedAt(new \DateTime()); |
90
|
|
|
try { |
91
|
37 |
|
$promises[] = $this->createPremise($download); |
92
|
11 |
|
} catch (\Exception $e) { |
93
|
11 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
94
|
11 |
|
$download->setMessage($e->getMessage()); |
95
|
11 |
|
$download->setFinishedAt(new \DateTime()); |
96
|
11 |
|
$this->handleCommand(new SaveDownloads([ |
97
|
11 |
|
$download |
98
|
|
|
])); |
99
|
37 |
|
continue; |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
|
103
|
37 |
|
return $promises; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* If none is internaly configured a new instance will be created. |
108
|
|
|
* |
109
|
|
|
* @return \GuzzleHttp\ClientInterface |
110
|
|
|
*/ |
111
|
60 |
|
public function getClient() |
112
|
|
|
{ |
113
|
60 |
|
if (!$this->client) { |
114
|
2 |
|
$fs = new Local(TARTANA_PATH_ROOT . '/var/tmp/'); |
115
|
2 |
|
$name = strtolower((new \ReflectionClass($this))->getShortName()) . '.cookie'; |
116
|
2 |
|
if (!$fs->has($name) || $this->getConfiguration()->get('clearSession', false)) { |
117
|
2 |
|
$fs->write($name, '', new Config()); |
118
|
|
|
} |
119
|
2 |
|
$this->client = new Client([ |
120
|
2 |
|
'cookies' => new FileCookieJar($fs->applyPathPrefix($name), true) |
121
|
|
|
]); |
122
|
|
|
} |
123
|
60 |
|
return $this->client; |
124
|
|
|
} |
125
|
|
|
|
126
|
77 |
|
public function setClient(ClientInterface $client = null) |
127
|
|
|
{ |
128
|
77 |
|
$this->client = $client; |
129
|
77 |
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Returns the configuration. |
133
|
|
|
* |
134
|
|
|
* @return \Joomla\Registry\Registry |
135
|
|
|
*/ |
136
|
35 |
|
protected function getConfiguration() |
137
|
|
|
{ |
138
|
35 |
|
return $this->configuration; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Returns the real url to download, subclasses can do here some |
143
|
|
|
* preprocessing of the given download. |
144
|
|
|
* The download will be saved after that operation. If null is returned, the |
145
|
|
|
* download will not be performed. |
146
|
|
|
* |
147
|
|
|
* @param Download $download |
148
|
|
|
* @return string |
149
|
|
|
*/ |
150
|
18 |
|
protected function getUrlToDownload(Download $download) |
151
|
|
|
{ |
152
|
18 |
|
return $download->getLink(); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Login function which can be used on subclasses to authenticate before the |
157
|
|
|
* download is done. |
158
|
|
|
* |
159
|
|
|
* @return boolean |
160
|
|
|
*/ |
161
|
23 |
|
protected function login() |
162
|
|
|
{ |
163
|
23 |
|
return true; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Returns if the local client has a cookie with the given name and is not |
168
|
|
|
* expired. |
169
|
|
|
* |
170
|
|
|
* @param string $name |
171
|
|
|
* @return \GuzzleHttp\Cookie\SetCookie |
172
|
|
|
*/ |
173
|
21 |
|
protected function getCookie($name) |
174
|
|
|
{ |
175
|
21 |
|
$cookies = $this->getClient()->getConfig('cookies'); |
176
|
|
|
|
177
|
21 |
|
if (!$cookies instanceof \Traversable) { |
178
|
11 |
|
return null; |
179
|
|
|
} |
180
|
|
|
|
181
|
10 |
|
foreach ($cookies as $cookie) { |
182
|
|
|
/** @var \GuzzleHttp\Cookie\SetCookie $cookie */ |
183
|
10 |
|
if ($cookie->getName() != $name) { |
184
|
2 |
|
continue; |
185
|
|
|
} |
186
|
8 |
|
if (!$cookie->getExpires() || $cookie->getExpires() > time()) { |
187
|
8 |
|
return $cookie; |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
|
|
191
|
4 |
|
return null; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Subclasses can define here the headers before the file is downloaded. |
196
|
|
|
* It must return an array of headers. |
197
|
|
|
* |
198
|
|
|
* @param Download $download |
199
|
|
|
* @return array |
200
|
|
|
*/ |
201
|
25 |
|
protected function getHeadersForDownload(Download $download) |
202
|
|
|
{ |
203
|
25 |
|
return []; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Parses the file name from a response. |
208
|
|
|
* Mainly it tryes to analyze the headers. |
209
|
|
|
* |
210
|
|
|
* @param Response $response |
211
|
|
|
* @return string|NULL |
212
|
|
|
*/ |
213
|
30 |
|
protected function parseFileName(Response $response) |
214
|
|
|
{ |
215
|
30 |
|
$dispHeader = $response->getHeader('Content-Disposition'); |
216
|
30 |
|
if ($dispHeader && preg_match('/.*filename=([^ ]+)/', $dispHeader[0], $matches)) { |
217
|
23 |
|
return trim($matches[1], '";'); |
218
|
|
|
} |
219
|
7 |
|
return null; |
220
|
|
|
} |
221
|
|
|
|
222
|
37 |
|
private function createPremise(Download $download) |
223
|
|
|
{ |
224
|
37 |
|
$url = $this->getUrlToDownload($download); |
225
|
36 |
|
if (!$url) { |
226
|
8 |
|
if (!$download->getMessage()) { |
227
|
8 |
|
$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_FAILED_REAL_URL'); |
228
|
|
|
} |
229
|
8 |
|
throw new \Exception($download->getMessage()); |
230
|
|
|
} |
231
|
|
|
|
232
|
28 |
|
$tmpFileName = 'tmp-' . $download->getId() . '.bin'; |
233
|
|
|
|
234
|
28 |
|
$me = $this; |
235
|
28 |
|
$fs = new Local($download->getDestination()); |
236
|
|
|
|
237
|
26 |
|
$this->log('Downloading download ' . $download->getId() . ' to ' . $fs->applyPathPrefix($tmpFileName)); |
238
|
|
|
|
239
|
|
|
// @codeCoverageIgnoreStart |
240
|
|
|
$options = [ |
241
|
|
|
RequestOptions::SINK => $fs->applyPathPrefix($tmpFileName), |
242
|
|
|
RequestOptions::PROGRESS => function ($totalSize, $downloadedSize) use ($download, $me) { |
243
|
|
|
if (!$downloadedSize || !$totalSize) { |
244
|
|
|
return; |
245
|
|
|
} |
246
|
|
|
$progress = (100 / $totalSize) * $downloadedSize; |
247
|
|
|
|
248
|
|
|
if ($progress < $download->getProgress() + (rand(100, 700) / 1000)) { |
249
|
|
|
// Reducing write transactions on the |
250
|
|
|
// repository |
251
|
|
|
return; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$download->setProgress($progress); |
255
|
|
|
$download->setSize($totalSize); |
256
|
|
|
$me->handleCommand(new SaveDownloads([ |
257
|
|
|
$download |
258
|
|
|
])); |
259
|
|
|
} |
260
|
|
|
]; |
261
|
|
|
if (defined('CURLOPT_TCP_KEEPALIVE')) { |
262
|
|
|
// Needed to keep the session alive on slow downloads |
263
|
|
|
$options['curl'] = [ |
264
|
|
|
CURLOPT_TCP_KEEPALIVE => 1, |
265
|
|
|
CURLOPT_TCP_KEEPIDLE => 30, |
266
|
|
|
CURLOPT_TCP_KEEPINTVL => 15 |
267
|
|
|
]; |
268
|
|
|
} |
269
|
|
|
// @codeCoverageIgnoreEnd |
270
|
|
|
|
271
|
26 |
|
if ($this->getConfiguration()->get('speedlimit') > 0) { |
272
|
2 |
|
$options['curl'][CURLOPT_MAX_RECV_SPEED_LARGE] = $this->getConfiguration()->get('speedlimit') * 1000; |
273
|
|
|
} |
274
|
|
|
|
275
|
26 |
|
$options[RequestOptions::HEADERS] = $this->getHeadersForDownload($download); |
276
|
|
|
|
277
|
26 |
|
$request = new Request('get', $url); |
278
|
26 |
|
$promise = $this->getClient()->sendAsync($request, $options); |
279
|
|
|
|
280
|
26 |
|
$promise->then( |
281
|
|
|
function (Response $resp) use ($fs, $tmpFileName, $download, $me) { |
282
|
24 |
|
$originalFileName = $this->parseFileName($resp); |
283
|
24 |
|
if (empty($download->getFileName()) && !empty($originalFileName)) { |
284
|
19 |
|
$download->setFileName($originalFileName); |
285
|
|
|
} |
286
|
|
|
|
287
|
24 |
|
if (!empty($download->getFileName())) { |
288
|
22 |
|
$fs->rename($tmpFileName, $download->getFileName()); |
289
|
|
|
} else { |
290
|
2 |
|
$download->setFileName($tmpFileName); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
// Hash check |
294
|
24 |
|
if (!empty($download->getHash())) { |
295
|
6 |
|
$hash = md5_file($fs->applyPathPrefix($download->getFileName())); |
296
|
6 |
|
if ($download->getHash() != $hash) { |
297
|
2 |
|
$fs->delete($download->getFileName()); |
298
|
2 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
299
|
2 |
|
$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_HASH'); |
300
|
2 |
|
$download->setFinishedAt(new \DateTime()); |
301
|
2 |
|
$me->handleCommand(new SaveDownloads([ |
302
|
2 |
|
$download |
303
|
|
|
])); |
304
|
2 |
|
return; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
308
|
22 |
|
$download->setState(Download::STATE_DOWNLOADING_COMPLETED); |
309
|
22 |
|
$download->setProgress(100); |
310
|
22 |
|
$download->setFinishedAt(new \DateTime()); |
311
|
22 |
|
$me->handleCommand(new SaveDownloads([ |
312
|
22 |
|
$download |
313
|
|
|
])); |
314
|
26 |
|
}, |
315
|
26 |
|
function (RequestException $e) use ($download, $me) { |
316
|
2 |
|
$download->setState(Download::STATE_DOWNLOADING_ERROR); |
317
|
2 |
|
$download->setMessage($e->getMessage()); |
318
|
2 |
|
$download->setFinishedAt(new \DateTime()); |
319
|
2 |
|
$me->handleCommand(new SaveDownloads([ |
320
|
2 |
|
$download |
321
|
|
|
])); |
322
|
26 |
|
} |
323
|
|
|
); |
324
|
26 |
|
return $promise; |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.