Completed
Push — master ( 1e1aac...d6bf2c )
by C
05:36
created

Http::fetchLinkList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
ccs 2
cts 2
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
namespace Tartana\Host\Common;
3
use GuzzleHttp\Client;
4
use GuzzleHttp\ClientInterface;
5
use GuzzleHttp\Cookie\FileCookieJar;
6
use GuzzleHttp\Exception\RequestException;
7
use GuzzleHttp\Psr7\Request;
8
use GuzzleHttp\Psr7\Response;
9
use GuzzleHttp\RequestOptions;
10
use Joomla\Registry\Registry;
11
use League\Flysystem\Adapter\Local;
12
use League\Flysystem\Config;
13
use Tartana\Domain\Command\SaveDownloads;
14
use Tartana\Entity\Download;
15
use Tartana\Host\HostInterface;
16
use Tartana\Mixins\CommandBusAwareTrait;
17
use Tartana\Mixins\LoggerAwareTrait;
18
19
class Http implements HostInterface
20
{
21
	use LoggerAwareTrait;
22
	use CommandBusAwareTrait;
23
24
	private $configuration = null;
25
26
	private $client = null;
27
28 75
	public function __construct (Registry $configuration, ClientInterface $client = null)
29
	{
30 75
		$this->configuration = $configuration;
31 75
		$this->setClient($client);
32 75
	}
33
34 3
	public function fetchLinkList ($link)
35
	{
36
		return [
37 3
				$link
38
		];
39
	}
40
41 7
	public function fetchDownloadInfo (array $downloads)
42
	{
43 7
		foreach ($downloads as $download)
44
		{
45
			// Connection check
46
			try
47
			{
48 7
				$originalName = $this->parseFileName($this->getClient()
49 7
					->head($download->getLink()));
50 5
				if (! empty($originalName) && empty($download->getFileName()))
51
				{
52 5
					$download->setFileName($originalName);
53
				}
54
			}
55 2
			catch (\Exception $e)
56
			{
57 2
				$this->log('Exception fetching head for connection test: ' . $e->getMessage());
58 2
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_URL');
59 7
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
60
			}
61
		}
62 7
	}
63
64 46
	public function download (array $downloads)
65
	{
66 46
		if (empty($downloads))
67
		{
68 2
			return [];
69
		}
70
71
		try
72
		{
73 44
			if (! $this->login())
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
74
			{
75 5
				foreach ($downloads as $download)
76
				{
77 5
					$download->setState(Download::STATE_DOWNLOADING_ERROR);
78 5
					$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_LOGIN');
79
				}
80 5
				$this->handleCommand(new SaveDownloads($downloads));
81 42
				return [];
82
			}
83
		}
84 2
		catch (\Exception $e)
85
		{
86 2
			foreach ($downloads as $download)
87
			{
88 2
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
89 2
				$download->setMessage($e->getMessage());
90
			}
91 2
			$this->handleCommand(new SaveDownloads($downloads));
92 2
			return [];
93
		}
94
95 37
		$promises = [];
96 37
		foreach ($downloads as $download)
97
		{
98 37
			$download->setStartedAt(new \DateTime());
99
			try
100
			{
101 37
				$promises[] = $this->createPremise($download);
102
			}
103 11
			catch (\Exception $e)
104
			{
105 11
				$download->setState(Download::STATE_DOWNLOADING_ERROR);
106 11
				$download->setMessage($e->getMessage());
107 11
				$download->setFinishedAt(new \DateTime());
108 11
				$this->handleCommand(new SaveDownloads([
109 11
						$download
110
				]));
111 37
				continue;
112
			}
113
		}
114
115 37
		return $promises;
116
	}
117
118
	/**
119
	 * If none is internaly configured a new instance will be created.
120
	 *
121
	 * @return \GuzzleHttp\ClientInterface
122
	 */
123 58
	public function getClient ()
124
	{
125 58
		if (! $this->client)
126
		{
127 2
			$fs = new Local(TARTANA_PATH_ROOT . '/var/tmp/');
128 2
			$name = strtolower((new \ReflectionClass($this))->getShortName()) . '.cookie';
129 2
			if (! $fs->has($name) || $this->getConfiguration()->get('clearSession', false))
130
			{
131 2
				$fs->write($name, '', new Config());
132
			}
133 2
			$this->client = new Client([
134 2
					'cookies' => new FileCookieJar($fs->applyPathPrefix($name), true)
135
			]);
136
		}
137 58
		return $this->client;
138
	}
139
140 75
	public function setClient (ClientInterface $client = null)
141
	{
142 75
		$this->client = $client;
143 75
	}
144
145
	/**
146
	 * Returns the configuration.
147
	 *
148
	 * @return \Joomla\Registry\Registry
149
	 */
150 35
	protected function getConfiguration ()
151
	{
152 35
		return $this->configuration;
153
	}
154
155
	/**
156
	 * Returns the real url to download, subclasses can do here some
157
	 * preprocessing of the given download.
158
	 * The download will be saved after that operation. If null is returned, the
159
	 * download will not be performed.
160
	 *
161
	 * @param Download $download
162
	 * @return string
163
	 */
164 18
	protected function getUrlToDownload (Download $download)
165
	{
166 18
		return $download->getLink();
167
	}
168
169
	/**
170
	 * Login function which can be used on subclasses to authenticate before the
171
	 * download is done.
172
	 *
173
	 * @return boolean
174
	 */
175 23
	protected function login ()
176
	{
177 23
		return true;
178
	}
179
180
	/**
181
	 * Returns if the local client has a cookie with the given name and is not
182
	 * expired.
183
	 *
184
	 * @param string $name
185
	 * @return \GuzzleHttp\Cookie\SetCookie
186
	 */
187 21
	protected function getCookie ($name)
188
	{
189 21
		$cookies = $this->getClient()->getConfig('cookies');
190
191 21
		if (! $cookies instanceof \Traversable)
192
		{
193 11
			return null;
194
		}
195
196 10
		foreach ($cookies as $cookie)
197
		{
198
			/** @var \GuzzleHttp\Cookie\SetCookie $cookie */
199 10
			if ($cookie->getName() != $name)
200
			{
201 2
				continue;
202
			}
203 8
			if (! $cookie->getExpires() || $cookie->getExpires() > time())
204
			{
205 8
				return $cookie;
206
			}
207
		}
208
209 4
		return null;
210
	}
211
212
	/**
213
	 * Subclasses can define here the headers before the file is downloaded.
214
	 * It must return an array of headers.
215
	 *
216
	 * @param Download $download
217
	 * @return array
218
	 */
219 25
	protected function getHeadersForDownload (Download $download)
220
	{
221 25
		return [];
222
	}
223
224
	/**
225
	 * Parses the file name from a response.
226
	 * Mainly it tryes to analyze the headers.
227
	 *
228
	 * @param Response $response
229
	 * @return string|NULL
230
	 */
231 28
	protected function parseFileName (Response $response)
232
	{
233 28
		$dispHeader = $response->getHeader('Content-Disposition');
234 28
		if ($dispHeader && preg_match('/.*filename=([^ ]+)/', $dispHeader[0], $matches))
235
		{
236 23
			return trim($matches[1], '";');
237
		}
238 5
		return null;
239
	}
240
241 37
	private function createPremise (Download $download)
242
	{
243 37
		$url = $this->getUrlToDownload($download);
244 36
		if (! $url)
245
		{
246 8
			if (! $download->getMessage())
247
			{
248 8
				$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_FAILED_REAL_URL');
249
			}
250 8
			throw new \Exception($download->getMessage());
251
		}
252
253 28
		$tmpFileName = 'tmp-' . $download->getId() . '.bin';
254
255 28
		$me = $this;
256 28
		$fs = new Local($download->getDestination());
257
258 26
		$this->log('Downloading download ' . $download->getId() . ' to ' . $fs->applyPathPrefix($tmpFileName));
259
260
		// @codeCoverageIgnoreStart
261
		$options = [
262
				RequestOptions::SINK => $fs->applyPathPrefix($tmpFileName),
263
				RequestOptions::PROGRESS => function  ($totalSize, $downloadedSize) use ( $download, $me) {
264
					if (! $downloadedSize || ! $totalSize)
265
					{
266
						return;
267
					}
268
					$progress = (100 / $totalSize) * $downloadedSize;
269
270
					if ($progress < $download->getProgress() + (rand(100, 700) / 1000))
271
					{
272
						// Reducing write transactions on the
273
						// repository
274
						return;
275
					}
276
277
					$download->setProgress($progress);
278
					$download->setSize($totalSize);
279
					$me->handleCommand(new SaveDownloads([
280
							$download
281
					]));
282
				}
283
		];
284
		if (defined('CURLOPT_TCP_KEEPALIVE'))
285
		{
286
			// Needed to keep the session alive on slow downloads
287
			$options['curl'] = [
288
					CURLOPT_TCP_KEEPALIVE => 1,
289
					CURLOPT_TCP_KEEPIDLE => 30,
290
					CURLOPT_TCP_KEEPINTVL => 15
291
			];
292
		}
293
		// @codeCoverageIgnoreEnd
294
295 26
		if ($this->getConfiguration()->get('speedlimit') > 0)
296
		{
297 2
			$options['curl'][CURLOPT_MAX_RECV_SPEED_LARGE] = $this->getConfiguration()->get('speedlimit') * 1000;
298
		}
299
300 26
		$options[RequestOptions::HEADERS] = $this->getHeadersForDownload($download);
301
302 26
		$request = new Request('get', $url);
303 26
		$promise = $this->getClient()->sendAsync($request, $options);
304
305 26
		$promise->then(
306
				function  (Response $resp) use ( $fs, $tmpFileName, $download, $me) {
307 24
					$originalFileName = $this->parseFileName($resp);
308 24
					if (empty($download->getFileName()) && ! empty($originalFileName))
309
					{
310 19
						$download->setFileName($originalFileName);
311
					}
312
313 24
					if (! empty($download->getFileName()))
314
					{
315 22
						$fs->rename($tmpFileName, $download->getFileName());
316
					}
317
					else
318
					{
319 2
						$download->setFileName($tmpFileName);
320
					}
321
322
					// Hash check
323 24
					if (! empty($download->getHash()))
324
					{
325 6
						$hash = md5_file($fs->applyPathPrefix($download->getFileName()));
326 6
						if ($download->getHash() != $hash)
327
						{
328 2
							$fs->delete($download->getFileName());
329 2
							$download->setState(Download::STATE_DOWNLOADING_ERROR);
330 2
							$download->setMessage('TARTANA_DOWNLOAD_MESSAGE_INVALID_HASH');
331 2
							$download->setFinishedAt(new \DateTime());
332 2
							$me->handleCommand(new SaveDownloads([
333 2
									$download
334
							]));
335 2
							return;
336
						}
337
					}
338
339 22
					$download->setState(Download::STATE_DOWNLOADING_COMPLETED);
340 22
					$download->setProgress(100);
341 22
					$download->setFinishedAt(new \DateTime());
342 22
					$me->handleCommand(new SaveDownloads([
343 22
							$download
344
					]));
345 26
				},
346 26
				function  (RequestException $e) use ( $download, $me) {
347 2
					$download->setState(Download::STATE_DOWNLOADING_ERROR);
348 2
					$download->setMessage($e->getMessage());
349 2
					$download->setFinishedAt(new \DateTime());
350 2
					$me->handleCommand(new SaveDownloads([
351 2
							$download
352
					]));
353 26
				});
354 26
		return $promise;
355
	}
356
}
357