Passed
Push — master ( b8e16f...40853b )
by Carlos C
02:32
created

Updater   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 287
Duplicated Lines 0 %

Test Coverage

Coverage 99.31%

Importance

Changes 0
Metric Value
dl 0
loc 287
ccs 144
cts 145
cp 0.9931
rs 9.2
c 0
b 0
f 0
wmc 34

26 Methods

Rating   Name   Duplication   Size   Complexity  
A setDownloader() 0 3 1
A hasImporter() 0 3 1
A indexUrl() 0 3 1
A gateways() 0 3 1
A runBlobs() 0 10 2
A version() 0 3 1
A date() 0 3 1
A logger() 0 3 1
A createReaderForPackedFile() 0 10 1
B processBlob() 0 37 2
A progress() 0 3 1
B processEnd() 0 24 2
A commandPaths() 0 9 1
A processReader() 0 3 1
A buildIndexUrl() 0 3 1
A importer() 0 6 2
A setIndexInterpreter() 0 3 1
A checkFileMd5() 0 9 2
A setProgress() 0 3 1
A checkCommands() 0 5 3
A processBegin() 0 23 2
A downloader() 0 3 1
A indexInterpreter() 0 3 1
A run() 0 17 1
A setLogger() 0 3 1
A __construct() 0 10 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpCfdi\RfcLinc\Updater;
6
7
use EngineWorks\ProgressStatus\NullProgress;
8
use EngineWorks\ProgressStatus\ProgressInterface;
9
use PhpCfdi\RfcLinc\DataGateway\FactoryInterface;
10
use PhpCfdi\RfcLinc\Domain\Catalog;
11
use PhpCfdi\RfcLinc\Domain\VersionDate;
12
use PhpCfdi\RfcLinc\Downloader\DownloaderInterface;
13
use PhpCfdi\RfcLinc\Downloader\PhpDownloader;
14
use PhpCfdi\RfcLinc\Util\CommandReader;
15
use PhpCfdi\RfcLinc\Util\ReaderInterface;
16
use PhpCfdi\RfcLinc\Util\ShellWhich;
17
use PhpCfdi\RfcLinc\Util\TemporaryFilename;
18
use Psr\Log\LoggerInterface;
19
use Psr\Log\NullLogger;
20
21
class Updater
22
{
23
    const URL_BLOBS_LIST = 'https://cfdisat.blob.core.windows.net/lco?restype=container&comp=list';
24
25
    /** @var VersionDate */
26
    private $date;
27
28
    /** @var FactoryInterface */
29
    private $gateways;
30
31
    /** @var DownloaderInterface */
32
    private $downloader;
33
34
    /** @var IndexInterpreter */
35
    private $indexInterpreter;
36
37
    /** @var Importer|null */
38
    private $importer;
39
40
    /** @var LoggerInterface */
41
    private $logger;
42
43
    /** @var ProgressInterface */
44
    private $progress;
45
46
    /** @var string[] */
47
    private $commands;
48
49 12
    public function __construct(VersionDate $date, FactoryInterface $gateways)
50
    {
51 12
        $this->date = $date;
52 12
        $this->gateways = $gateways;
53 12
        $this->downloader = new PhpDownloader();
54 12
        $this->indexInterpreter = new IndexInterpreter();
55 12
        $this->logger = new NullLogger();
56 12
        $this->progress = new NullProgress();
57 12
        $this->commands = $this->commandPaths();
58 12
        $this->checkCommands();
59 12
    }
60
61 12
    public function commandPaths(): array
62
    {
63 12
        $which = new ShellWhich();
64
        $commands = [
65 12
            'gunzip' => $which('gunzip'),
66 12
            'openssl' => $which('openssl'),
67 12
            'iconv' => $which('iconv'),
68
        ];
69 12
        return $commands;
70
    }
71
72 12
    public function checkCommands()
73
    {
74 12
        foreach ($this->commands as $command => $path) {
75 12
            if ('' === $path) {
76 12
                throw new \InvalidArgumentException("Cannot find $command, it is required to update");
77
            }
78
        }
79 12
    }
80
81 1
    public function hasImporter(): bool
82
    {
83 1
        return ($this->importer instanceof Importer);
84
    }
85
86 3
    public function importer(): Importer
87
    {
88 3
        if ($this->importer instanceof Importer) {
89 2
            return $this->importer;
90
        }
91 1
        throw new \LogicException('There is no importer, did you call run() method?');
92
    }
93
94 2
    public function version(): Catalog
95
    {
96 2
        return $this->importer()->catalog();
97
    }
98
99 3
    public function progress(): ProgressInterface
100
    {
101 3
        return $this->progress;
102
    }
103
104 1
    public function date(): VersionDate
105
    {
106 1
        return $this->date;
107
    }
108
109 1
    public function gateways(): FactoryInterface
110
    {
111 1
        return $this->gateways;
112
    }
113
114 2
    public function downloader(): DownloaderInterface
115
    {
116 2
        return $this->downloader;
117
    }
118
119 2
    public function indexInterpreter(): IndexInterpreter
120
    {
121 2
        return $this->indexInterpreter;
122
    }
123
124 2
    public function logger(): LoggerInterface
125
    {
126 2
        return $this->logger;
127
    }
128
129 2
    public function setDownloader(DownloaderInterface $downloader)
130
    {
131 2
        $this->downloader = $downloader;
132 2
    }
133
134 1
    public function setIndexInterpreter(IndexInterpreter $indexInterpreter)
135
    {
136 1
        $this->indexInterpreter = $indexInterpreter;
137 1
    }
138
139 2
    public function setLogger(LoggerInterface $logger)
140
    {
141 2
        $this->logger = $logger;
142 2
    }
143
144 2
    public function setProgress(ProgressInterface $progress)
145
    {
146 2
        $this->progress = $progress;
147 2
    }
148
149 1
    public function run(): int
150
    {
151 1
        $indexUrl = $this->indexUrl();
152 1
        $this->logger->info("Processing {$indexUrl}...");
153
154 1
        $this->logger->debug("Downloading {$indexUrl}...");
155 1
        $indexContents = $this->downloader->download($indexUrl);
156
157 1
        $this->logger->debug('Obtaining blobs...');
158 1
        $blobs = $this->indexInterpreter->obtainBlobs($indexContents);
159 1
        $blobsCount = count($blobs);
160
161 1
        $this->logger->debug("Processing $blobsCount blobs...");
162 1
        $processedLines = $this->runBlobs(...$blobs);
163
164 1
        $this->logger->info(sprintf('Processed %s lines', number_format($processedLines)));
165 1
        return $processedLines;
166
    }
167
168 1
    public function runBlobs(Blob ...$blobs): int
169
    {
170 1
        $processedLines = 0;
171 1
        $this->processBegin();
172 1
        foreach ($blobs as $blob) {
173 1
            $processedLines = $processedLines + $this->processBlob($blob);
174
        }
175 1
        $this->processEnd();
176
177 1
        return $processedLines;
178
    }
179
180 2
    public function processBegin()
181
    {
182 2
        $this->logger->notice('Starting general process...');
183
184
        // obtain or create version
185 2
        $gwCatalogs = $this->gateways->catalog();
186 2
        if ($gwCatalogs->exists($this->date)) {
187
            throw new \RuntimeException('The version is already in the catalog, it was not expected to exists');
188
        }
189
        // start optimizations
190 2
        $this->gateways->optimizer()->prepare();
191
192
        // create and store version
193 2
        $catalog = new Catalog($this->date, 0, 0, 0, 0);
194 2
        $gwCatalogs->insert($catalog);
195
196
        // create importer
197 2
        $this->importer = new Importer($catalog, $this->gateways, $this->progress());
198
199
        // set all records as deleted
200 2
        $this->gateways->listedRfc()->markAllAsDeleted();
201
202 2
        $this->logger->debug('General process started');
203 2
    }
204
205 1
    public function processBlob(Blob $blob): int
206
    {
207
        // create temp file
208 1
        $downloaded = new TemporaryFilename();
209 1
        $filename = (string) $downloaded;
210 1
        $url = $blob->url();
211 1
        $expectedMd5 = $blob->md5();
212
213 1
        $this->logger->info("Downloading $url...");
214
215
        // download the resourse
216 1
        $this->logger->debug("Downloading $url into $filename...");
217 1
        $downloadStart = time();
218 1
        $this->downloader->downloadAs($url, $filename);
219 1
        $downloadElapsed = time() - $downloadStart;
220 1
        $this->logger->debug("Download $url takes $downloadElapsed seconds");
221
222
        // check the md5 checksum
223 1
        if ('' !== $expectedMd5) {
224 1
            $this->logger->debug("Checking $expectedMd5 on $filename...");
225 1
            $this->checkFileMd5($filename, $expectedMd5);
226
        }
227
228
        // process file
229 1
        $this->logger->debug("Opening $filename (as packed data)...");
230 1
        $reader = $this->createReaderForPackedFile($filename);
231 1
        $processedLines = $this->processReader($reader);
232 1
        $this->logger->debug("Closing $filename...");
233 1
        $reader->close();
234
235 1
        $this->logger->notice(sprintf('Blob %s process %s lines', $url, number_format($processedLines)));
236
237
        // clear the resource
238 1
        $this->logger->debug("Removing $filename...");
239 1
        $downloaded->unlink();
240
241 1
        return $processedLines;
242
    }
243
244 2
    public function processReader(ReaderInterface $reader): int
245
    {
246 2
        return $this->importer()->importReader($reader);
247
    }
248
249 2
    public function processEnd()
250
    {
251 2
        $importer = $this->importer();
252 2
        $catalog = $importer->catalog();
253 2
        $gwRfc = $this->gateways->listedRfc();
254
255
        // count how many were deleted and log
256 2
        $this->logger->debug('Checking deletes...');
257 2
        foreach ($gwRfc->eachDeleted() as $rfc) {
258 1
            $importer->performDelete($rfc);
259
        }
260 2
        $this->logger->debug(sprintf('Found %s lines deleted', number_format($catalog->deleted())));
261
262 2
        $active = $gwRfc->countDeleted(false);
263 2
        $catalog->setRecords($active);
264 2
        $this->logger->info(sprintf('Found %s RFC active', number_format($active)));
265
266
        // store current version
267 2
        $this->logger->debug('Saving version...');
268 2
        $this->gateways->catalog()->update($catalog);
269
270
        // end optimizations
271 2
        $this->gateways->optimizer()->finish();
272 2
        $this->logger->notice('General process finish');
273 2
    }
274
275 2
    public function checkFileMd5(string $filename, string $expectedMd5)
276
    {
277
        // check md5
278 2
        $md5file = (string) md5_file($filename);
279 2
        if ($md5file !== $expectedMd5) {
280 1
            throw new \RuntimeException(sprintf(
281 1
                'The MD5 from file "%s" does not match with "%s"',
282 1
                $md5file,
283 1
                $expectedMd5
284
            ));
285
        }
286 2
    }
287
288 1
    public function createReaderForPackedFile(string $filename): ReaderInterface
289
    {
290 1
        $reader = new CommandReader();
291 1
        $command = implode(' | ', [
292 1
            $this->commands['gunzip'] . ' --stdout ' . escapeshellarg($filename),
293 1
            $this->commands['openssl'] . ' smime -verify -in - -inform der -noverify 2> /dev/null',
294 1
            $this->commands['iconv'] . ' --from iso8859-1 --to utf-8',
295
        ]);
296 1
        $reader->open($command);
297 1
        return $reader;
298
    }
299
300 1
    public function indexUrl(): string
301
    {
302 1
        return $this->buildIndexUrl($this->date);
303
    }
304
305 1
    public static function buildIndexUrl(VersionDate $date): string
306
    {
307 1
        return static::URL_BLOBS_LIST . '&prefix=l_RFC_' . $date->format('_');
308
    }
309
}
310