Completed
Push — ezp25946-migrate_files_to_othe... ( ed29e0...c5a18a )
by
unknown
16:39
created

MigrateFilesCommand::outputConfiguredHandlers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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