Completed
Pull Request — master (#31)
by Hiraku
02:32
created

ParallelDownloader::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
crap 1
1
<?php
2
/*
3
 * hirak/prestissimo
4
 * @author Hiraku NAKANO
5
 * @license MIT https://github.com/hirak/prestissimo
6
 */
7
namespace Hirak\Prestissimo;
8
9
use Composer\Package;
10
use Composer\IO;
11
use Composer\Config;
12
13
/**
14
 *
15
 */
16
class ParallelDownloader
17
{
18
    /** @var IO/IOInterface */
19
    protected $io;
20
21
    /** @var Config */
22
    protected $config;
23
24
    /** @var int */
25
    protected $totalCnt = 0;
26
    protected $successCnt = 0;
27
    protected $failureCnt = 0;
28
29 1
    public function __construct(IO\IOInterface $io, Config $config)
30
    {
31 1
        $this->io = $io;
32 1
        $this->config = $config;
33 1
    }
34
35
    /**
36
     * @param Package\PackageInterface[] $packages
37
     * @param array $pluginConfig
38
     * @return void
39
     */
40
    public function download(array $packages, array $pluginConfig)
41
    {
42
        $mh = curl_multi_init();
43
        $unused = array();
44
        $maxConns = $pluginConfig['maxConnections'];
45
        for ($i = 0; $i < $maxConns; ++$i) {
46
            $unused[] = curl_init();
47
        }
48
49
        // @codeCoverageIgnoreStart
50
        if (function_exists('curl_share_init')) {
51
            $sh = curl_share_init();
52
            curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
53
54
            foreach ($unused as $ch) {
55
                curl_setopt($ch, CURLOPT_SHARE, $sh);
56
            }
57
        }
58
59
        if (function_exists('curl_multi_setopt')) {
60
            if ($pluginConfig['pipeline']) {
61
                curl_multi_setopt($mh, CURLMOPT_PIPELINING, true);
62
            }
63
        }
64
        // @codeCoverageIgnoreEnd
65
66
        $cachedir = rtrim($this->config->get('cache-files-dir'), '\/');
67
68
        $chFpMap = array();
69
        $running = 0; //ref type
70
        $remains = 0; //ref type
71
72
        $this->totalCnt = count($packages);
73
        $this->successCnt = 0;
74
        $this->failureCnt = 0;
75
        $this->io->write("    Prefetch start: <comment>success: $this->successCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
76
        do {
77
            // prepare curl resources
78
            while (count($unused) > 0 && count($packages) > 0) {
79
                $package = array_pop($packages);
80
                $filepath = $cachedir . DIRECTORY_SEPARATOR . static::getCacheKey($package);
81
                if (file_exists($filepath)) {
82
                    ++$this->successCnt;
83
                    continue;
84
                }
85
                $ch = array_pop($unused);
86
87
                // make file resource
88
                $chFpMap[(int)$ch] = $outputFile = new OutputFile($filepath);
89
90
                // make url
91
                $url = $package->getDistUrl();
92
                $host = parse_url($url, PHP_URL_HOST) ?: '';
93
                $request = new Aspects\HttpGetRequest($host, $url, $this->io);
94
                $request->verbose = $pluginConfig['verbose'];
95
                if (in_array($package->getName(), $pluginConfig['privatePackages'])) {
96
                    $request->maybePublic = false;
97
                } else {
98
                    $request->maybePublic = (bool)preg_match('%^(?:https|git)://github\.com%', $package->getSourceUrl());
99
                }
100
                $onPreDownload = Factory::getPreEvent($request);
101
                $onPreDownload->notify();
102
103
                $opts = $request->getCurlOpts();
104
                if ($pluginConfig['insecure']) {
105
                    $opts[CURLOPT_SSL_VERIFYPEER] = false;
106
                }
107
                if (! empty($pluginConfig['capath'])) {
108
                    $opts[CURLOPT_CAPATH] = $pluginConfig['capath'];
109
                }
110
                unset($opts[CURLOPT_ENCODING]);
111
                unset($opts[CURLOPT_USERPWD]); // ParallelDownloader doesn't support private packages.
112
                curl_setopt_array($ch, $opts);
113
                curl_setopt($ch, CURLOPT_FILE, $outputFile->getPointer());
114
                curl_multi_add_handle($mh, $ch);
115
            }
116
117
            // wait for any event
118
            do {
119
                // start multi download
120
                do {
121
                    $stat = curl_multi_exec($mh, $running);
122
                } while ($stat === CURLM_CALL_MULTI_PERFORM);
123
124
                switch (curl_multi_select($mh, 5)) {
125
                case -1:
126
                    usleep(250);
127
                    // fall through
128
                case 0:
129
                    continue 2;
130
                default:
131
                    do {
132
                        $stat = curl_multi_exec($mh, $running);
133
                    } while ($stat === CURLM_CALL_MULTI_PERFORM);
134
135
                    do {
136
                        if ($raised = curl_multi_info_read($mh, $remains)) {
137
                            $ch = $raised['handle'];
138
                            $errno = curl_errno($ch);
139
                            $info = curl_getinfo($ch);
140
                            curl_setopt($ch, CURLOPT_FILE, STDOUT);
141
                            $index = (int)$ch;
142
                            $outputFile = $chFpMap[$index];
143
                            unset($chFpMap[$index]);
144
                            if (CURLE_OK === $errno && 200 === $info['http_code']) {
145
                                ++$this->successCnt;
146
                            } else {
147
                                ++$this->failureCnt;
148
                                $outputFile->setFailure();
149
                            }
150
                            unset($outputFile);
151
                            $this->io->write($this->makeDownloadingText($info['url']));
152
                            curl_multi_remove_handle($mh, $ch);
153
                            $unused[] = $ch;
154
                        }
155
                    } while ($remains > 0);
156
157
                    if (count($packages) > 0) {
158
                        break 2;
159
                    }
160
                }
161
            } while ($running);
162
        } while (count($packages) > 0);
163
        $this->io->write("    Finished: <comment>success: $this->successCnt, failure: $this->failureCnt, total: $this->totalCnt</comment>");
164
165
        foreach ($unused as $ch) {
166
            curl_close($ch);
167
        }
168
        curl_multi_close($mh);
169
    }
170
171
    /**
172
     * @param string $url
173
     * @return string
174
     */
175
    private function makeDownloadingText($url)
176
    {
177
        $request = new Aspects\HttpGetRequest('example.com', $url, $this->io);
178
        $request->query = array();
179
        return "    <comment>$this->successCnt/$this->totalCnt</comment>:    {$request->getURL()}";
180
    }
181
182
    public static function getCacheKey(Package\PackageInterface $p)
183
    {
184
        $distRef = $p->getDistReference();
185
        if (preg_match('{^[a-f0-9]{40}$}', $distRef)) {
186
            return "{$p->getName()}/$distRef.{$p->getDistType()}";
187
        }
188
189
        return "{$p->getName()}/{$p->getVersion()}-$distRef.{$p->getDistType()}";
190
    }
191
}
192