Completed
Push — 4-cactus ( 289e92...0bcdf5 )
by
unknown
21s queued 17s
created

StreamsShell::updateStreamMetadata()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 3
eloc 11
c 2
b 0
f 1
nc 4
nop 1
dl 0
loc 21
rs 9.9
1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2019 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
namespace BEdita\Core\Shell;
14
15
use BEdita\Core\Model\Entity\Stream;
16
use Cake\Console\ConsoleOptionParser;
17
use Cake\Console\Shell;
18
use Cake\Database\Expression\QueryExpression;
19
use Cake\ORM\Query;
20
21
/**
22
 * Stream shell commands: removeOrphans
23
 *
24
 * @since 4.0.0
25
 * @property \BEdita\Core\Model\Table\StreamsTable $Streams
26
 */
27
class StreamsShell extends Shell
28
{
29
    /**
30
     * @inheritDoc
31
     */
32
    public $modelClass = 'Streams';
33
34
    /**
35
     * {@inheritDoc}
36
     *
37
     * @codeCoverageIgnore
38
     */
39
    public function getOptionParser(): ConsoleOptionParser
40
    {
41
        $parser = parent::getOptionParser();
42
        $parser->addSubcommand('removeOrphans', [
43
            'help' => 'remove obsolete/orphans streams and related files',
44
            'parser' => [
45
                'description' => [
46
                    'Remove orphans streams.',
47
                ],
48
                'options' => [
49
                    'days' => [
50
                        'help' => 'Days to consider for stream research for orphans (remove data older than specified days)',
51
                        'required' => false,
52
                        'default' => 1,
53
                    ],
54
                ],
55
            ],
56
        ]);
57
        $parser->addSubcommand('refreshMetadata', [
58
            'help' => 'read streams metadata from file and update database information',
59
            'parser' => [
60
                'description' => [
61
                    'Refresh streams metadata in database.',
62
                ],
63
                'options' => [
64
                    'force' => [
65
                        'help' => 'Force refreshing all streams, not only those with empty metadata',
66
                        'required' => false,
67
                        'default' => false,
68
                        'boolean' => true,
69
                    ],
70
                ],
71
            ],
72
        ]);
73
74
        return $parser;
75
    }
76
77
    /**
78
     * Remove orphans older than specified days (default: older than 1 day)
79
     *
80
     * @return void
81
     */
82
    public function removeOrphans()
83
    {
84
        $days = (int)$this->param('days');
85
        $query = $this->Streams->find()
86
            ->where([
87
                'object_id IS NULL',
88
                'created <' => \Cake\I18n\FrozenTime::now()->subDays($days),
89
            ]);
90
        $count = 0;
91
        foreach ($query as $stream) {
92
            $this->verbose(sprintf('Deleting stream %s...', $stream->id));
93
            $this->Streams->deleteOrFail($stream);
94
            $count++;
95
        }
96
        $this->out(sprintf('%d stream(s) deleted', $count));
97
    }
98
99
    /**
100
     * Re-read streams metadata from file and update information in database.
101
     *
102
     * @return void
103
     */
104
    public function refreshMetadata()
105
    {
106
        $query = $this->Streams->find('all');
107
        if ((bool)$this->param('force') === false) {
108
            $query = $query->where(function (QueryExpression $exp): QueryExpression {
109
                return $exp->or_(function (QueryExpression $exp): QueryExpression {
110
                    return $exp
111
                        ->eq($this->Streams->aliasField('file_size'), 0)
112
                        ->isNull($this->Streams->aliasField('width'))
113
                        ->isNull($this->Streams->aliasField('height'));
114
                });
115
            });
116
        }
117
118
        $count = $query->count();
119
        $this->info(sprintf('Approximately %d streams to be processed', $count));
120
        $success = 0;
121
122
        foreach ($this->streamsGenerator($query) as $stream) {
123
            if ($this->updateStreamMetadata($stream)) {
0 ignored issues
show
Bug introduced by
$stream of type Cake\Datasource\ResultSetInterface is incompatible with the type BEdita\Core\Model\Entity\Stream expected by parameter $stream of BEdita\Core\Shell\Stream...:updateStreamMetadata(). ( Ignorable by Annotation )

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

123
            if ($this->updateStreamMetadata(/** @scrutinizer ignore-type */ $stream)) {
Loading history...
124
                $success++;
125
            }
126
        }
127
128
        $this->info(sprintf('Refresh completed: %d streams updated successfully, %d failed', $success, ($count - $success)));
129
    }
130
131
    /**
132
     * Update stream metadata.
133
     *
134
     * @param Stream $stream The stream to update
135
     * @return bool Success status of the operation
136
     */
137
    protected function updateStreamMetadata(Stream $stream): bool
138
    {
139
        try {
140
            // Read current file's content...
141
            $content = $stream->contents;
142
            if ($content === null) {
143
                $this->warn(sprintf('  stream %s (object %d) is empty or could not be read', $stream->uuid, $stream->object_id));
144
145
                return false;
146
            }
147
148
            // ...and write it back, triggering Stream model's methods to read metadata from file
149
            $stream->contents = $content;
150
            $this->Streams->saveOrFail($stream);
151
        } catch (\Throwable $t) {
152
            $this->err(sprintf('  error updating stream %s (object %d): %s', $stream->uuid, $stream->object_id, $t->getMessage()));
153
154
            return false;
155
        }
156
157
        return true;
158
    }
159
160
    /**
161
     * Generator to paginate through all streams.
162
     *
163
     * @param \Cake\ORM\Query $query Query to retrieve concerned streams
164
     * @param int $limit Limit amount of objects retrieved with each internal iteration
165
     * @return \Generator|Stream[]
166
     */
167
    protected function streamsGenerator(Query $query, int $limit = 100): \Generator
168
    {
169
        // Although `uuid` is not a monotonically increasing field, we will at most skip the streams that are created
170
        // AFTER we launch the script, and whose UUID is lexicographically less than the one we are currently
171
        // checking — but we still cover all streams created before our script starts!
172
        $query = $query->orderAsc($this->Streams->aliasField('uuid'));
173
        $q = clone $query;
174
        do {
175
            $results = $q->limit($limit)->all();
176
            if ($results->isEmpty()) {
177
                break;
178
            }
179
180
            yield from $results;
181
182
            /** @var Stream $last */
183
            $last = $results->last();
184
            $q = clone $query;
185
            $q = $q->where(function (QueryExpression $exp) use ($last): QueryExpression {
186
                return $exp->gt($this->Streams->aliasField('uuid'), $last->uuid);
187
            });
188
        } while ($q->count() > 0);
189
    }
190
}
191