Passed
Push — trunk ( 6d115e...2fe92c )
by Christian
11:00 queued 15s
created

DeleteNotUsedMediaCommand::dryRun()   B

Complexity

Conditions 9
Paths 3

Size

Total Lines 67
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 41
nc 3
nop 2
dl 0
loc 67
rs 7.7084
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Shopware\Core\Content\Media\Commands;
4
5
use Doctrine\DBAL\Connection;
6
use Shopware\Core\Content\Media\MediaEntity;
0 ignored issues
show
Bug introduced by
The type Shopware\Core\Content\Media\MediaEntity was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Shopware\Core\Content\Media\UnusedMediaPurger;
8
use Shopware\Core\Framework\Adapter\Console\ShopwareStyle;
9
use Shopware\Core\Framework\Log\Package;
10
use Shopware\Core\Framework\Util\MemorySizeCalculator;
11
use Symfony\Component\Console\Attribute\AsCommand;
12
use Symfony\Component\Console\Command\Command;
0 ignored issues
show
Bug introduced by
The type Symfony\Component\Console\Command\Command was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Symfony\Component\Console\Cursor;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Input\InputOption;
16
use Symfony\Component\Console\Output\OutputInterface;
17
18
#[AsCommand(
19
    name: 'media:delete-unused',
20
    description: 'Deletes all media files which are not used in any entity',
21
)]
22
#[Package('content')]
23
class DeleteNotUsedMediaCommand extends Command
24
{
25
    /**
26
     * @internal
27
     */
28
    public function __construct(
29
        private readonly UnusedMediaPurger $unusedMediaPurger,
30
        private readonly Connection $connection
31
    ) {
32
        parent::__construct();
33
    }
34
35
    /**
36
     * {@inheritdoc}
37
     */
38
    protected function configure(): void
39
    {
40
        $this->addOption('folder-entity', null, InputOption::VALUE_REQUIRED, 'Restrict deletion of not used media in default location folders of the provided entity name');
41
        $this->addOption('limit', null, InputOption::VALUE_OPTIONAL, 'The limit of media entries to query');
42
        $this->addOption('offset', null, InputOption::VALUE_OPTIONAL, 'The offset to start from');
43
        $this->addOption('grace-period-days', null, InputOption::VALUE_REQUIRED, 'The offset to start from', 20);
44
        $this->addOption('dry-run', description: 'Show list of files to be deleted');
45
    }
46
47
    /**
48
     * {@inheritdoc}
49
     */
50
    protected function execute(InputInterface $input, OutputInterface $output): int
51
    {
52
        $io = new ShopwareStyle($input, $output);
53
54
        try {
55
            $this->connection->fetchOne('SELECT JSON_OVERLAPS(JSON_ARRAY(1), JSON_ARRAY(1));');
56
        } catch (\Exception $e) {
57
            $io->error('Your database does not support the JSON_OVERLAPS function. Please update your database to MySQL 8.0 or MariaDB 10.9 or higher.');
58
59
            return self::FAILURE;
60
        }
61
62
        if ($input->getOption('dry-run')) {
63
            return $this->dryRun($input, $output);
64
        }
65
66
        $confirm = $io->confirm('Are you sure that you want to delete unused media files?', false);
67
68
        if (!$confirm) {
69
            $io->caution('Aborting due to user input.');
70
71
            return self::SUCCESS;
72
        }
73
74
        $count = $this->unusedMediaPurger->deleteNotUsedMedia(
75
            $input->getOption('limit') ? (int) $input->getOption('limit') : null,
76
            $input->getOption('offset') ? (int) $input->getOption('offset') : null,
77
            (int) $input->getOption('grace-period-days'),
78
            $input->getOption('folder-entity'),
79
        );
80
81
        if ($count === 0) {
82
            $io->success(sprintf('There are no unused media files uploaded before the grace period of %d days.', (int) $input->getOption('grace-period-days')));
83
84
            return self::SUCCESS;
85
        }
86
87
        $io->success(sprintf('Successfully deleted %d media files.', $count));
88
89
        return self::SUCCESS;
90
    }
91
92
    private function dryRun(InputInterface $input, OutputInterface $output): int
93
    {
94
        $cursor = new Cursor($output);
95
96
        $io = new ShopwareStyle($input, $output);
97
98
        $mediaBatches = $this->unusedMediaPurger->getNotUsedMedia(
99
            $input->getOption('limit') ? (int) $input->getOption('limit') : 50,
100
            $input->getOption('offset') ? (int) $input->getOption('offset') : null,
101
            (int) $input->getOption('grace-period-days'),
102
            $input->getOption('folder-entity'),
103
        );
104
105
        $totalCount = 0;
106
        $finished = $this->consumeGeneratorInBatches($mediaBatches, 20, function ($batchNum, array $medias) use ($io, $cursor, &$totalCount) {
107
            if ($batchNum === 0 && \count($medias) === 0) {
108
                return true;
109
            }
110
111
            if ($batchNum === 0) {
112
                //we only clear the screen when we actually have some unused media
113
                $cursor->clearScreen();
114
            }
115
116
            $totalCount += \count($medias);
117
118
            $cursor->moveToPosition(0, 0);
119
            $cursor->clearOutput();
120
            $io->title(
121
                sprintf(
122
                    'Files that will be deleted: Page %d. Records: %d - %d',
123
                    $batchNum + 1,
124
                    ($batchNum * 20) + 1,
125
                    $batchNum * 20 + \count($medias)
126
                )
127
            );
128
129
            $io->table(
130
                ['Filename', 'Title', 'Uploaded At', 'File Size'],
131
                array_map(
132
                    fn (MediaEntity $media) => [
133
                        $media->getFileNameIncludingExtension(),
134
                        $media->getTitle(),
135
                        $media->getUploadedAt()?->format('F jS, Y'),
136
                        MemorySizeCalculator::formatToBytes($media->getFileSize() ?? 0),
137
                    ],
138
                    $medias
139
                )
140
            );
141
142
            if (\count($medias) < 20) {
143
                //last batch
144
                return true;
145
            }
146
147
            return $io->confirm('Show next page?', false);
148
        });
149
150
        if ($totalCount === 0) {
151
            $io->success(sprintf('There are no unused media files uploaded before the grace period of %d days.', (int) $input->getOption('grace-period-days')));
152
        } elseif ($finished) {
153
            $io->success('No more files to show.');
154
        } else {
155
            $io->info('Aborting.');
156
        }
157
158
        return self::SUCCESS;
159
    }
160
161
    /**
162
     * Given a generator which yields arrays of items, this method will consume the generator in batches of the given size.
163
     *
164
     * @param callable(int, array<mixed>): bool $callback
165
     */
166
    private function consumeGeneratorInBatches(\Generator $generator, int $batchSize, callable $callback): bool
167
    {
168
        $i = 0;
169
        $batch = [];
170
        foreach ($generator as $items) {
171
            $batch = array_merge($batch, $items);
172
173
            while (\count($batch) >= $batchSize) {
174
                $continue = $callback($i++, array_splice($batch, 0, $batchSize));
175
176
                if (!$continue) {
177
                    return false;
178
                }
179
            }
180
        }
181
182
        //last remaining batch
183
        if (\count($batch) > 0) {
184
            return $callback($i++, $batch);
185
        }
186
187
        return true;
188
    }
189
}
190