Completed
Push — feature/decoupled ( b00aba...a10681 )
by Webysther
02:10
created

Create::switch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 0
dl 0
loc 11
ccs 0
cts 7
cp 0
crap 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
/*
6
 * This file is part of the Packagist Mirror.
7
 *
8
 * For the full license information, please view the LICENSE.md
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webs\Mirror\Command;
13
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Webs\Mirror\ShortName;
17
use Webs\Mirror\Provider;
18
use stdClass;
19
use Generator;
20
use Closure;
21
22
/**
23
 * Create a mirror.
24
 *
25
 * @author Webysther Nunes <[email protected]>
26
 */
27
class Create extends Base
28
{
29
    use ShortName;
30
31
    /**
32
     * @var stdClass
33
     */
34
    protected $providers;
35
36
    /**
37
     * @var array
38
     */
39
    protected $providerIncludes;
40
41
    /**
42
     * @var string
43
     */
44
    protected $currentProvider;
45
46
    /**
47
     * @var array
48
     */
49
    protected $providerPackages;
50
51
    /**
52
     * @var Clean
53
     */
54
    protected $clean;
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function __construct($name = '')
60
    {
61
        parent::__construct('create');
62
        $this->setDescription(
63
            'Create/update packagist mirror'
64
        );
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function execute(InputInterface $input, OutputInterface $output):int
71
    {
72
        $this->progressBar->setConsole($input, $output);
73
        $this->package->setConsole($input, $output);
74
        $this->package->setHttp($this->http);
75
        $this->provider->setConsole($input, $output);
76
        $this->provider->setHttp($this->http);
77
78
        // Download providers, with repository, is incremental
79
        if ($this->downloadProviders()->stop()) {
80
            return $this->getExitCode();
81
        }
82
83
        // Download packages
84
        if ($this->downloadPackages()->stop()) {
85
            return $this->getExitCode();
86
        }
87
88
        // If .packages.json dont exists
89
        if ($this->filesystem->has(self::DOT)) {
90
            // Move to new location
91
            $this->filesystem->move(self::DOT, self::MAIN);
0 ignored issues
show
Unused Code introduced by
The call to Webs\Mirror\Filesystem::move() has too many arguments starting with self::MAIN. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

91
            $this->filesystem->/** @scrutinizer ignore-call */ 
92
                               move(self::DOT, self::MAIN);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
92
        }
93
94
        $this->setExitCode($this->clean->execute($input, $output));
95
96
        if ($this->initialized) {
97
            $this->filesystem->delete(self::INIT);
98
        }
99
100
        return $this->getExitCode();
101
    }
102
103
    /**
104
     * @param Clean $clean
105
     */
106
    public function setClean(Clean $clean):Create
107
    {
108
        $this->clean = $clean;
109
110
        return $this;
111
    }
112
113
    /**
114
     * @return int
115
     */
116
    protected function getExitCode():int
117
    {
118
        $this->generateHtml();
119
120
        return isset($this->exitCode) ? $this->exitCode : 0;
121
    }
122
123
    /**
124
     * Check if packages.json was changed.
125
     *
126
     * @return bool
127
     */
128
    protected function isEqual():bool
129
    {
130
        // if 'p/...' folder not found
131
        if (!$this->filesystem->has(self::TO)) {
132
            $this->filesystem->touch(self::INIT);
133
        }
134
135
        $this->initialized = $this->filesystem->has(self::INIT);
136
137
        $newPackages = json_encode($this->providers, JSON_PRETTY_PRINT);
138
139
        // No provider changed? Just relax...
140
        if ($this->canSkip(self::MAIN)) {
141
            $old = $this->filesystem->getHashFile(self::MAIN);
142
            $new = $this->filesystem->getHash($newPackages);
143
144
            if ($old == $new) {
145
                $this->output->writeln(self::MAIN.' <info>updated</>');
146
                $this->setExitCode(0);
147
148
                return true;
149
            }
150
        }
151
152
        $this->filesystem->write(self::DOT, $newPackages);
153
154
        return false;
155
    }
156
157
    /**
158
     * Download packages.json & provider-xxx$xxx.json.
159
     *
160
     * @return Create
161
     */
162
    protected function downloadProviders():Create
163
    {
164
        $this->output->writeln(
165
            'Loading providers from <info>'.$this->http->getBaseUri().'</>'
166
        );
167
168
        $this->providers = $this->provider->addFullPath(
169
            $this->package->loadMainJson()
170
        );
171
172
173
        if ($this->isEqual()) {
174
            return $this;
175
        }
176
177
        $this->providerIncludes = $this->provider->normalize($this->providers);
178
        $generator = $this->getProvidersGenerator();
179
180
        $this->progressBar->start(count($this->providerIncludes));
181
182
        $success = function($body, $path) {
183
            $this->filesystem->write($path, $body);
184
        };
185
186
        $this->http->pool($generator, $success, $this->getClosureComplete());
187
        $this->progressBar->end();
188
        $this->showErrors();
189
190
        if ($generator->getReturn() && !$this->initialized) {
1 ignored issue
show
Bug introduced by
The method getReturn() does not exist on Generator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

190
        if ($generator->/** @scrutinizer ignore-call */ getReturn() && !$this->initialized) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
191
            $this->output->writeln('All providers are <info>updated</>');
192
193
            return $this->setExitCode(0);
194
        }
195
196
        return $this;
197
    }
198
199
    /**
200
     * @return Closure
201
     */
202
    protected function getClosureComplete():Closure
203
    {
204
        return function() {
205
            $this->progressBar->progress();
206
        };
207
    }
208
209
    /**
210
     * Download packages.json & provider-xxx$xxx.json.
211
     *
212
     * @return Generator Providers downloaded
213
     */
214
    protected function getProvidersGenerator():Generator
215
    {
216
        $providerIncludes = array_keys($this->providerIncludes);
217
        $updated = true;
218
        foreach ($providerIncludes as $uri) {
219
            if ($this->filesystem->has($uri)) {
220
                continue;
221
            }
222
223
            $updated = false;
224
            yield $uri => $this->http->getRequest($uri);
225
        }
226
227
        return $updated;
228
    }
229
230
    /**
231
     * @param string $path
232
     *
233
     * @return bool
234
     */
235
    protected function canSkip(string $path):bool
236
    {
237
        if ($this->filesystem->has($path) && !$this->initialized) {
238
            return true;
239
        }
240
241
        return false;
242
    }
243
244
    /**
245
     * Show errors.
246
     *
247
     * @return Create
248
     */
249
    protected function showErrors():Create
250
    {
251
        if (!$this->isVerbose()) {
252
            return $this;
253
        }
254
255
        $errors = $this->http->getPoolErrors();
256
        if (count($errors) === 0) {
257
            return $this;
258
        }
259
260
        foreach ($errors as $path => $reason) {
261
            $shortname = $this->shortname($path);
262
            $error = $reason->getCode();
263
            $host = $reason->getRequest()->getUri()->getHost();
264
265
            $this->output->write(
266
                "<comment>$shortname</> failed from ".
267
                "<info>$host</> with HTTP error"
268
            );
269
270
            if (!$error) {
271
                $this->output->writeln(
272
                    ':'.PHP_EOL.'<error>'.$reason->getMessage().'</>'
273
                );
274
                continue;
275
            }
276
277
            $this->output->writeln(" <error>$error</>");
278
        }
279
280
        $this->output->write(PHP_EOL);
281
282
        return $this;
283
    }
284
285
    /**
286
     * Disable mirror when due lots of errors.
287
     */
288
    protected function disableDueErrors()
289
    {
290
        $errors = $this->http->getPoolErrors();
291
        if (count($errors) === 0) {
292
            return $this;
293
        }
294
295
        $counter = [];
296
297
        foreach ($errors as $reason) {
298
            $uri = $reason->getRequest()->getUri();
299
            $host = $uri->getScheme().'://'.$uri->getHost();
300
301
            if (!isset($counter[$host])) {
302
                $counter[$host] = 0;
303
            }
304
305
            ++$counter[$host];
306
        }
307
308
        $mirrors = $this->http->getMirror()->toArray();
309
310
        foreach ($mirrors as $mirror) {
311
            $total = $counter[$mirror];
312
            if ($total < 1000) {
313
                continue;
314
            }
315
316
            $this->output->write(PHP_EOL);
317
            $this->output->writeln(
318
                '<error>Due to '.
319
                $total.' errors mirror '.
320
                $mirror.' will be disabled</>'
321
            );
322
            $this->output->write(PHP_EOL);
323
            $this->http->getMirror()->remove($mirror);
324
        }
325
326
        return $this;
327
    }
328
329
    /**
330
     * @param string $uri
331
     *
332
     * @return Create
333
     */
334
    protected function loadProviderPackages(string $uri):Create
335
    {
336
        $providers = json_decode($this->filesystem->read($uri))->providers;
337
        $this->providerPackages = $this->package->normalize($providers);
338
        $this->currentProvider = $uri;
339
340
        return $this;
341
    }
342
343
    /**
344
     * Download packages listed on provider-*.json on public/p dir.
345
     *
346
     * @return Create
347
     */
348
    protected function downloadPackages():Create
349
    {
350
        $totalProviders = count($this->providerIncludes);
351
        $currentProvider = 0;
352
353
        $providerIncludes = array_keys($this->providerIncludes);
354
        foreach ($providerIncludes as $uri) {
355
            $shortname = $this->shortname($uri);
356
357
            $this->output->writeln(
358
                '['.++$currentProvider.'/'.$totalProviders.']'.
359
                ' Loading packages from <info>'.$shortname.'</> provider'
360
            );
361
362
            $this->http->useMirrors();
363
            $generator = $this->loadProviderPackages($uri)->getPackagesGenerator();
364
            if (empty(iterator_to_array($generator))) {
365
                continue;
366
            }
367
368
            $this->progressBar->start(count($this->providerPackages));
369
            $this->poolPackages($generator);
370
            $this->progressBar->end();
371
            $this->showErrors()->disableDueErrors()->fallback();
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378
     * Download only a package.
379
     *
380
     * @return Generator Providers downloaded
381
     */
382
    protected function getPackagesGenerator():Generator
383
    {
384
        $providerPackages = array_keys($this->providerPackages);
385
        foreach ($providerPackages as $uri) {
386
            if ($this->filesystem->has($uri)) {
387
                $this->progressBar->progress();
388
                continue;
389
            }
390
391
            if ($this->initialized) {
392
                $uri = $this->http->getMirror()->getNext().'/'.$uri;
393
            }
394
395
            yield $uri => $this->http->getRequest($uri);
396
        }
397
    }
398
399
    /**
400
     * @param Generator $generator
401
     * @param bool|bool $useMirrors
402
     *
403
     * @return Create
404
     */
405
    protected function poolPackages(Generator $generator):Create
406
    {
407
        $this->http->pool(
408
            $generator,
409
            // Success
410
            function($body, $path) {
411
                $this->filesystem->write($path, $body);
412
                $this->package->setDownloaded($path);
413
            },
414
            // If complete, even failed and success
415
            $this->getClosureComplete()
416
        );
417
418
        return $this;
419
    }
420
421
    protected function fallback():Create
422
    {
423
        $total = count($this->http->getPoolErrors());
424
425
        if (!$total) {
426
            return $this;
427
        }
428
429
        $shortname = $this->shortname($this->currentProvider);
430
431
        $this->output->writeln(
432
            'Fallback packages from <info>'.$shortname.
433
            '</> provider to main mirror <info>'.$this->http->getBaseUri().'</>'
434
        );
435
436
        $this->providerPackages = $this->http->getPoolErrors();
437
        $generator = $this->getPackagesGenerator();
438
        $this->progressBar->start($total);
439
        $this->poolPackages($generator);
440
        $this->progressBar->end();
441
        $this->showErrors();
442
443
        return $this;
444
    }
445
446
    /**
447
     * Generate HTML of index.html.
448
     */
449
    protected function generateHtml():Create
450
    {
451
        ob_start();
452
        include getcwd().'/resources/index.html.php';
453
        file_put_contents('index.html', ob_get_clean());
454
455
        return $this;
456
    }
457
}
458