Completed
Branch dev (4bcb34)
by Darko
13:52
created

Backfill::safeBackfill()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 1
dl 0
loc 21
rs 9.3142
c 0
b 0
f 0
1
<?php
2
3
namespace Blacklight;
4
5
use App\Models\Group;
6
use Blacklight\db\DB;
7
use App\Models\Settings;
8
9
class Backfill
10
{
11
    /**
12
     * @var \Blacklight\db\DB
13
     */
14
    public $pdo;
15
16
    /**
17
     * @var
18
     */
19
    protected $_binaries;
20
21
    /**
22
     * @var \Blacklight\NNTP
23
     */
24
    protected $_nntp;
25
    /**
26
     * Should we use compression for headers?
27
     *
28
     * @var bool
29
     */
30
    protected $_compressedHeaders;
31
32
    /**
33
     * Log and or echo debug.
34
     * @var bool
35
     */
36
    protected $_debug = false;
37
38
    /**
39
     * Echo to cli?
40
     * @var bool
41
     */
42
    protected $_echoCLI;
43
44
    /**
45
     * How far back should we go on safe back fill?
46
     *
47
     * @var string
48
     */
49
    protected $_safeBackFillDate;
50
51
    /**
52
     * @var string
53
     */
54
    protected $_safePartRepair;
55
56
    /**
57
     * Should we disable the group if we have backfilled far enough?
58
     * @var bool
59
     */
60
    protected $_disableBackfillGroup;
61
62
    /**
63
     * Constructor.
64
     *
65
     * @param array $options Class instances / Echo to cli?
66
     *
67
     * @throws \Exception
68
     */
69
    public function __construct(array $options = [])
70
    {
71
        $defaults = [
72
            'Echo'      => true,
73
            'Logger'    => null,
74
            'Groups'    => null,
75
            'NNTP'      => null,
76
            'Settings'  => null,
77
        ];
78
        $options += $defaults;
79
80
        $this->_echoCLI = ($options['Echo'] && config('nntmux.echocli'));
81
82
        $this->pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB());
83
        $this->_nntp = (
84
            $options['NNTP'] instanceof NNTP
85
            ? $options['NNTP'] : new NNTP(['Settings' => $this->pdo])
86
        );
87
88
        $this->_compressedHeaders = (int) Settings::settingValue('..compressedheaders') === 1;
0 ignored issues
show
Bug introduced by
'..compressedheaders' of type string is incompatible with the type boolean|array expected by parameter $setting of App\Models\Settings::settingValue(). ( Ignorable by Annotation )

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

88
        $this->_compressedHeaders = (int) Settings::settingValue(/** @scrutinizer ignore-type */ '..compressedheaders') === 1;
Loading history...
89
        $this->_safeBackFillDate = Settings::settingValue('..safebackfilldate') !== '' ? (string) Settings::settingValue('safebackfilldate') : '2008-08-14';
90
        $this->_safePartRepair = (int) Settings::settingValue('..safepartrepair') === 1 ? 'update' : 'backfill';
91
        $this->_disableBackfillGroup = (int) Settings::settingValue('..disablebackfillgroup') === 1;
92
    }
93
94
    /**
95
     * Backfill all the groups up to user specified time/date.
96
     *
97
     * @param string     $groupName
98
     * @param string|int $articles
99
     * @param string     $type
100
     *
101
     * @return void
102
     * @throws \Exception
103
     */
104
    public function backfillAllGroups($groupName = '', $articles = '', $type = ''): void
105
    {
106
        $res = [];
107
        if ($groupName !== '') {
108
            $grp = Group::getByName($groupName);
109
            if ($grp) {
110
                $res = [$grp];
111
            }
112
        } else {
113
            $res = Group::getActiveBackfill($type);
114
        }
115
116
        $groupCount = count($res);
117
        if ($groupCount > 0) {
118
            $counter = 1;
119
            $allTime = microtime(true);
120
            $dMessage = (
121
                'Backfilling: '.
122
                $groupCount.
123
                ' group(s) - Using compression? '.
124
                ($this->_compressedHeaders ? 'Yes' : 'No')
125
            );
126
127
            if ($this->_echoCLI) {
128
                ColorCLI::doEcho(ColorCLI::header($dMessage), true);
129
            }
130
131
            $this->_binaries = new Binaries(
132
                ['NNTP' => $this->_nntp, 'Echo' => $this->_echoCLI, 'Settings' => $this->pdo]
133
            );
134
135
            if ($articles !== '' && ! is_numeric($articles)) {
136
                $articles = 20000;
137
            }
138
139
            // Loop through groups.
140
            foreach ($res as $groupArr) {
141
                if ($groupName === '') {
142
                    $dMessage = 'Starting group '.$counter.' of '.$groupCount;
143
144
                    if ($this->_echoCLI) {
145
                        ColorCLI::doEcho(ColorCLI::header($dMessage), true);
146
                    }
147
                }
148
                $this->backfillGroup($groupArr, $groupCount - $counter, $articles);
149
                $counter++;
150
            }
151
152
            $dMessage = 'Backfilling completed in '.number_format(microtime(true) - $allTime, 2).' seconds.';
153
154
            if ($this->_echoCLI) {
155
                ColorCLI::doEcho(ColorCLI::primary($dMessage), true);
156
            }
157
        } else {
158
            $dMessage = 'No groups specified. Ensure groups are added to database for updating.';
159
160
            if ($this->_echoCLI) {
161
                ColorCLI::doEcho(ColorCLI::warning($dMessage), true);
162
            }
163
        }
164
    }
165
166
    /**
167
     * Backfill single group.
168
     *
169
     * @param array      $groupArr
170
     * @param int        $left
171
     * @param int|string $articles
172
     *
173
     * @return void
174
     * @throws \Exception
175
     */
176
    public function backfillGroup($groupArr, $left, $articles = ''): void
177
    {
178
        // Start time for this group.
179
        $startGroup = microtime(true);
180
181
        $this->_binaries->logIndexerStart();
182
183
        $groupName = str_replace('alt.binaries', 'a.b', $groupArr['name']);
184
185
        // If our local oldest article 0, it means we never ran update_binaries on the group.
186
        if ($groupArr['first_record'] <= 0) {
187
            $dMessage =
188
                'You need to run update_binaries on '.
189
                $groupName.
190
                '. Otherwise the group is dead, you must disable it.';
191
192
            if ($this->_echoCLI) {
193
                ColorCLI::doEcho(ColorCLI::error($dMessage), true);
194
            }
195
196
            return;
197
        }
198
199
        // Select group, here, only once
200
        $data = $this->_nntp->selectGroup($groupArr['name']);
201
        if ($this->_nntp->isError($data)) {
202
            $data = $this->_nntp->dataError($this->_nntp, $groupArr['name']);
203
            if ($this->_nntp->isError($data)) {
204
                return;
205
            }
206
        }
207
208
        if ($this->_echoCLI) {
209
            ColorCLI::doEcho(ColorCLI::primary('Processing '.$groupName), true);
210
        }
211
212
        // Check if this is days or post backfill.
213
        $postCheck = $articles !== '';
214
215
        // Get target post based on date or user specified number.
216
        $targetpost = (string) (
217
            $postCheck
218
            ?
219
            round($groupArr['first_record'] - $articles)
220
            :
221
            $this->_binaries->daytopost($groupArr['backfill_target'], $data)
222
        );
223
224
        // Check if target post is smaller than server's oldest, set it to oldest if so.
225
        if ($targetpost < $data['first']) {
226
            $targetpost = $data['first'];
227
        }
228
229
        // Check if our target post is newer than our oldest post or if our local oldest article is older than the servers oldest.
230
        if ($targetpost >= $groupArr['first_record'] || $groupArr['first_record'] <= $data['first']) {
231
            $dMessage =
232
                'We have hit the maximum we can backfill for '.
233
                $groupName.
234
                ($this->_disableBackfillGroup ? ', disabling backfill on it.' :
235
                ', skipping it, consider disabling backfill on it.');
236
237
            if ($this->_disableBackfillGroup) {
238
                Group::updateGroupStatus($groupArr['id'], 'backfill', 0);
239
            }
240
241
            if ($this->_echoCLI) {
242
                ColorCLI::doEcho(ColorCLI::notice($dMessage), true);
243
            }
244
245
            return;
246
        }
247
248
        if ($this->_echoCLI) {
249
            ColorCLI::doEcho(
250
                ColorCLI::primary(
251
                    'Group '.
252
                    $groupName.
253
                    "'s oldest article is ".
254
                    number_format($data['first']).
255
                    ', newest is '.
256
                    number_format($data['last']).
257
                    ".\nOur target article is ".
258
                    number_format($targetpost).
259
                    '. Our oldest article is article '.
260
                    number_format($groupArr['first_record']).
261
                    '.'
262
                ), true
263
            );
264
        }
265
266
        // Set first and last, moving the window by max messages.
267
        $last = ($groupArr['first_record'] - 1);
268
        // Set the initial "chunk".
269
        $first = ($last - $this->_binaries->messageBuffer + 1);
270
271
        // Just in case this is the last chunk we needed.
272
        if ($targetpost > $first) {
273
            $first = $targetpost;
274
        }
275
276
        $done = false;
277
        while ($done === false) {
278
            if ($this->_echoCLI) {
279
                ColorCLI::doEcho(
280
                    color('Getting '.
281
                    number_format($last - $first + 1).
282
                    ' articles from '.
283
                    $groupName.
284
                    ', '.
285
                    $left.
286
                    ' group(s) left. ('.
287
                    number_format($first - $targetpost).
288
                    ' articles in queue')->fg('yellow'), true);
289
            }
290
291
            flush();
292
            $lastMsg = $this->_binaries->scan($groupArr, $first, $last, $this->_safePartRepair);
293
294
            // Get the oldest date.
295
            if (isset($lastMsg['firstArticleDate'])) {
296
                // Try to get it from the oldest pulled article.
297
                $newdate = strtotime($lastMsg['firstArticleDate']);
298
            } else {
299
                // If above failed, try to get it with postdate method.
300
                $newdate = $this->_binaries->postdate($first, $data);
301
            }
302
303
            $this->pdo->queryExec(
304
                sprintf(
305
                    '
306
					UPDATE groups
307
					SET first_record_postdate = %s, first_record = %s, last_updated = NOW()
308
					WHERE id = %d',
309
                    $this->pdo->from_unixtime($newdate),
310
                    $this->pdo->escapeString($first),
311
                    $groupArr['id']
312
                )
313
            );
314
            if ($first === $targetpost) {
315
                $done = true;
316
            } else {
317
                // Keep going: set new last, new first, check for last chunk.
318
                $last = ($first - 1);
319
                $first = ($last - $this->_binaries->messageBuffer + 1);
320
                if ($targetpost > $first) {
321
                    $first = $targetpost;
322
                }
323
            }
324
        }
325
326
        if ($this->_echoCLI) {
327
            ColorCLI::doEcho(
328
                ColorCLI::primary(
329
                    PHP_EOL.
330
                    'Group '.
331
                    $groupName.
332
                    ' processed in '.
333
                    number_format(microtime(true) - $startGroup, 2).
334
                    ' seconds.'
335
                ),
336
                true
337
            );
338
        }
339
    }
340
341
    /**
342
     * Safe backfill using posts. Going back to a date specified by the user on the site settings.
343
     * This does 1 group for x amount of parts until it reaches the date.
344
     *
345
     * @param string $articles
346
     *
347
     * @return void
348
     * @throws \Exception
349
     */
350
    public function safeBackfill($articles = ''): void
351
    {
352
        $groupname = $this->pdo->queryOneRow(
353
            sprintf(
354
                '
355
				SELECT name FROM groups
356
				WHERE first_record_postdate BETWEEN %s AND NOW()
357
				AND backfill = 1
358
				ORDER BY name ASC',
359
                $this->pdo->escapeString($this->_safeBackFillDate)
360
            )
361
        );
362
363
        if (! $groupname) {
364
            $dMessage =
365
                'No groups to backfill, they are all at the target date '.
366
                $this->_safeBackFillDate.
367
                ', or you have not enabled them to be backfilled in the groups page.'.PHP_EOL;
368
            exit($dMessage);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
369
        }
370
        $this->backfillAllGroups($groupname['name'], $articles);
371
    }
372
}
373