Completed
Push — 6.7 ( caaa8b...485307 )
by
unknown
19:19
created

MigrateFilesCommand::validateHandlerOptions()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 8
nop 3
dl 0
loc 36
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Bundle\EzPublishIOBundle\Command;
8
9
use eZ\Bundle\EzPublishIOBundle\Migration\FileListerRegistry;
10
use eZ\Bundle\EzPublishIOBundle\Migration\FileMigratorInterface;
11
use Symfony\Component\Console\Command\Command;
12
use Symfony\Component\Console\Input\InputInterface;
13
use Symfony\Component\Console\Input\InputOption;
14
use Symfony\Component\Console\Helper\ProgressBar;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Symfony\Component\Console\Question\ConfirmationQuestion;
17
18
final class MigrateFilesCommand extends Command
19
{
20
    /** @var mixed Configuration for metadata handlers */
21
    private $configuredMetadataHandlers;
22
23
    /** @var mixed Configuration for binary data handlers */
24
    private $configuredBinarydataHandlers;
25
26
    /** @var \eZ\Bundle\EzPublishIOBundle\Migration\FileListerRegistry */
27
    private $fileListerRegistry;
28
29
    /** @var \eZ\Bundle\EzPublishIOBundle\Migration\FileListerInterface[] */
30
    private $fileListers;
31
32
    /** @var \eZ\Bundle\EzPublishIOBundle\Migration\FileMigratorInterface */
33
    private $fileMigrator;
34
35
    public function __construct(
36
        array $configuredMetadataHandlers,
37
        array $configuredBinarydataHandlers,
38
        FileListerRegistry $fileListerRegistry,
39
        FileMigratorInterface $fileMigrator
40
    ) {
41
        $this->configuredMetadataHandlers = $configuredMetadataHandlers;
42
        $this->configuredBinarydataHandlers = $configuredBinarydataHandlers;
43
        if (!array_key_exists('default', $this->configuredMetadataHandlers)) {
44
            $this->configuredMetadataHandlers['default'] = [];
45
        }
46
        if (!array_key_exists('default', $this->configuredBinarydataHandlers)) {
47
            $this->configuredBinarydataHandlers['default'] = [];
48
        }
49
50
        $this->fileListerRegistry = $fileListerRegistry;
51
        $this->fileMigrator = $fileMigrator;
52
53
        foreach ($this->fileListerRegistry->getIdentifiers() as $fileListerIdentifier) {
54
            $this->fileListers[] = $this->fileListerRegistry->getItem($fileListerIdentifier);
55
        }
56
57
        parent::__construct();
58
    }
59
60
    protected function configure()
61
    {
62
        $this
63
            ->setName('ezplatform:io:migrate-files')
64
            ->setDescription('Migrates files from one IO repository to another')
65
            ->addOption('from', null, InputOption::VALUE_REQUIRED, 'Migrate from <from_metadata_handler>,<from_binarydata_handler>')
66
            ->addOption('to', null, InputOption::VALUE_REQUIRED, 'Migrate to <to_metadata_handler>,<to_binarydata_handler>')
67
            ->addOption('list-io-handlers', null, InputOption::VALUE_NONE, 'List available IO handlers')
68
            ->addOption('bulk-count', null, InputOption::VALUE_REQUIRED, 'Number of files processed at once', 100)
69
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Execute a dry run')
70
            ->setHelp(
71
                <<<EOT
72
The command <info>%command.name%</info> migrates files from one IO repository
73
to another. It can for example be used to migrate local files from the default
74
IO configuration to a new IO configuration, like a clustered setup.
75
76
<fg=red>NB: This command is experimental. Use with caution!</>
77
78
The <info>--from</info> and <info>--to</info> values must be specified as <info><metadata_handler>,<binarydata_handler></info>.
79
If <info>--from</info> is omitted, the default IO configuration will be used.
80
If <info>--to</info> is omitted, the first non-default IO configuration will be used.
81
82
<fg=red>During the script execution the files should not be modified. To avoid
83
surprises you are advised to create a backup and/or execute a dry run before
84
proceeding with actual update.</>
85
86
Since this script can potentially run for a very long time, to avoid memory
87
exhaustion run it in production environment using <info>--env=prod</info> switch.
88
89
EOT
90
            );
91
    }
92
93
    protected function execute(InputInterface $input, OutputInterface $output)
94
    {
95
        if ($input->getOption('list-io-handlers')) {
96
            $this->outputConfiguredHandlers($output);
97
98
            return;
99
        }
100
101
        $bulkCount = (int)$input->getOption('bulk-count');
102
        if ($bulkCount < 1) {
103
            $output->writeln('The value for --bulk-count must be a positive integer.');
104
105
            return;
106
        }
107
108
        $output->writeln($this->getProcessedHelp());
109
110
        $fromHandlers = $input->getOption('from') ? explode(',', $input->getOption('from')) : null;
111
        $toHandlers = $input->getOption('to') ? explode(',', $input->getOption('to')) : null;
112
113
        if (!$fromHandlers) {
114
            $fromHandlers = ['default', 'default'];
115
        }
116
        if (!$toHandlers) {
117
            $toHandlers = [
118
                array_keys($this->configuredMetadataHandlers)[0],
119
                array_keys($this->configuredBinarydataHandlers)[0],
120
            ];
121
        }
122
123
        if (!$this->validateHandlerOptions($fromHandlers, $toHandlers, $output)) {
124
            return;
125
        }
126
127
        $output->writeln([
128
            "Migrating from '$fromHandlers[0],$fromHandlers[1]' to '$toHandlers[0],$toHandlers[1]'",
129
            '',
130
        ]);
131
132
        $totalCount = 0;
133
        foreach ($this->fileListers as $fileLister) {
134
            $fileLister->setIODataHandlersByIdentifiers(
135
                $fromHandlers[0],
136
                $fromHandlers[1],
137
                $toHandlers[0],
138
                $toHandlers[1]
139
            );
140
141
            $totalCount += $fileLister->countFiles();
142
        }
143
        $this->fileMigrator->setIODataHandlersByIdentifiers(
144
            $fromHandlers[0],
145
            $fromHandlers[1],
146
            $toHandlers[0],
147
            $toHandlers[1]
148
        );
149
150
        $output->writeln([
151
            'Total number of files to migrate: ' . ($totalCount === null ? 'unknown' : $totalCount),
152
            'This number does not include image aliases, but they will also be migrated.',
153
            '',
154
        ]);
155
156
        if ($totalCount === 0) {
157
            $output->writeln('Nothing to process.');
158
159
            return;
160
        }
161
162
        $helper = $this->getHelper('question');
163
        $question = new ConfirmationQuestion(
164
            '<question>Are you sure you want to proceed?</question> ',
165
            false
166
        );
167
168
        if (!$helper->ask($input, $output, $question)) {
169
            $output->writeln('Aborting.');
170
171
            return;
172
        }
173
174
        $this->migrateFiles(
175
            $totalCount,
176
            $bulkCount,
177
            $input->getOption('dry-run'),
178
            $output
179
        );
180
    }
181
182
    /**
183
     * Output the configured meta/binary data handlers.
184
     *
185
     * @param OutputInterface $output
186
     */
187
    protected function outputConfiguredHandlers(OutputInterface $output)
188
    {
189
        $output->writeln(
190
            'Configured meta data handlers: ' . implode(', ', array_keys($this->configuredMetadataHandlers))
191
        );
192
        $output->writeln(
193
            'Configured binary data handlers: ' . implode(', ', array_keys($this->configuredBinarydataHandlers))
194
        );
195
    }
196
197
    /**
198
     * Verify that the handler options have been set to meaningful values.
199
     *
200
     * @param mixed $fromHandlers
201
     * @param mixed $toHandlers
202
     * @param OutputInterface $output
203
     * @return bool
204
     */
205
    protected function validateHandlerOptions(
206
        $fromHandlers,
207
        $toHandlers,
208
        OutputInterface $output
209
    ) {
210
        foreach (['From' => $fromHandlers, 'To' => $toHandlers] as $direction => $handlers) {
211
            $lowerDirection = strtolower($direction);
212
            if (count($handlers) !== 2) {
213
                $output->writeln(
214
                    "Enter two comma separated values for the --$lowerDirection option: " .
215
                    "<{$lowerDirection}_metadata_handler>,<{$lowerDirection}_binarydata_handler>"
216
                );
217
218
                return false;
219
            }
220
221
            foreach (['meta' => $handlers[0], 'binary' => $handlers[1]] as $fileDataType => $handler) {
222
                if (!in_array($handler, array_keys(
223
                    $fileDataType === 'meta' ? $this->configuredMetadataHandlers : $this->configuredBinarydataHandlers
224
                ))) {
225
                    $output->writeln("$direction $fileDataType data handler '$handler' is not configured.");
226
                    $this->outputConfiguredHandlers($output);
227
228
                    return false;
229
                }
230
            }
231
        }
232
233
        if ($fromHandlers === $toHandlers) {
234
            $output->writeln('From and to handlers are the same. Nothing to do.');
235
236
            return false;
237
        }
238
239
        return true;
240
    }
241
242
    /**
243
     * Migrate files.
244
     *
245
     * @param int|null $totalFileCount Total count of files, null if unknown
246
     * @param int $bulkCount Number of files to process in each batch
247
     * @param bool $dryRun
248
     * @param OutputInterface $output
249
     */
250
    protected function migrateFiles(
251
        $totalFileCount = null,
252
        $bulkCount,
253
        $dryRun,
254
        OutputInterface $output
255
    ) {
256
        $progress = new ProgressBar($output, $totalFileCount);
257
        if ($totalFileCount) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $totalFileCount of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
258
            $progress->setFormat("%message%\n %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%");
259
        } else {
260
            $progress->setFormat("%message%\n %current% [%bar%] %elapsed:6s% %memory:6s%");
261
        }
262
263
        $output->writeln('');
264
        $progress->setMessage('Starting migration...');
265
        $progress->start();
266
267
        $elapsedFileCount = 0;
268
        $timestamp = microtime(true);
269
        $updateFrequency = 1;
270
271
        foreach ($this->fileListers as $fileLister) {
272
            $pass = 0;
273
            do {
274
                $metadataList = $fileLister->loadMetadataList($bulkCount, $pass * $bulkCount);
275
276
                foreach ($metadataList as $metadata) {
277
                    if (!$dryRun) {
278
                        $this->fileMigrator->migrateFile($metadata);
279
                    }
280
281
                    $progress->setMessage('Updated file ' . $metadata->id);
282
                    $progress->advance();
283
                    ++$elapsedFileCount;
284
285
                    // Magic that ensures the progressbar is updated ca. once per second
286
                    if (($elapsedFileCount % $updateFrequency) === 0) {
287
                        $newTimestamp = microtime(true);
288
                        if ($newTimestamp - $timestamp > 0.5 && $updateFrequency > 1) {
289
                            $updateFrequency = (int)($updateFrequency / 2);
290
                            $progress->setRedrawFrequency($updateFrequency);
291
                        } elseif ($newTimestamp - $timestamp < 0.1 && $updateFrequency < 10000) {
292
                            $updateFrequency = $updateFrequency * 2;
293
                            $progress->setRedrawFrequency($updateFrequency);
294
                        }
295
                        $timestamp = $newTimestamp;
296
                    }
297
                }
298
299
                ++$pass;
300
            } while (count($metadataList) > 0);
301
        }
302
303
        $progress->setMessage('');
304
        $progress->finish();
305
306
        $output->writeln("\n\nFinished processing $elapsedFileCount files.");
307
        if ($totalFileCount && $totalFileCount > $elapsedFileCount) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $totalFileCount of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
308
            $output->writeln([
309
                'Files that could not be migrated: ' . ($totalFileCount - $elapsedFileCount),
310
                '',
311
            ]);
312
        }
313
    }
314
}
315