Passed
Push — master ( a761e5...06eb23 )
by Morris
28:30 queued 11:17
created

Repair::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
nc 1
nop 4
dl 0
loc 9
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2020, Morris Jobke <[email protected]>
6
 *
7
 * @author Morris Jobke <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
 *
24
 */
25
26
namespace OC\Core\Command\Preview;
27
28
use bantu\IniGetWrapper\IniGetWrapper;
29
use OC\Preview\Storage\Root;
30
use OCP\Files\Folder;
31
use OCP\Files\IRootFolder;
32
use OCP\Files\NotFoundException;
33
use OCP\IConfig;
34
use OCP\ILogger;
35
use Symfony\Component\Console\Command\Command;
36
use Symfony\Component\Console\Helper\ProgressBar;
37
use Symfony\Component\Console\Input\InputInterface;
38
use Symfony\Component\Console\Input\InputOption;
39
use Symfony\Component\Console\Output\OutputInterface;
40
use Symfony\Component\Console\Question\ConfirmationQuestion;
41
42
class Repair extends Command {
43
	/** @var IConfig */
44
	protected $config;
45
	/** @var IRootFolder */
46
	private $rootFolder;
47
	/** @var ILogger */
48
	private $logger;
49
50
	/** @var bool */
51
	private $stopSignalReceived = false;
52
	/** @var int */
53
	private $memoryLimit;
54
	/** @var int */
55
	private $memoryTreshold;
56
57
	public function __construct(IConfig $config, IRootFolder $rootFolder, ILogger $logger, IniGetWrapper $phpIni) {
58
		$this->config = $config;
59
		$this->rootFolder = $rootFolder;
60
		$this->logger = $logger;
61
62
		$this->memoryLimit = $phpIni->getBytes('memory_limit');
63
		$this->memoryTreshold = $this->memoryLimit - 25 * 1024 * 1024;
64
65
		parent::__construct();
66
	}
67
68
	protected function configure() {
69
		$this
70
			->setName('preview:repair')
71
			->setDescription('distributes the existing previews into subfolders')
72
			->addOption('batch', 'b', InputOption::VALUE_NONE, 'Batch mode - will not ask to start the migration and start it right away.')
73
			->addOption('dry', 'd', InputOption::VALUE_NONE, 'Dry mode - will not create, move or delete any files - in combination with the verbose mode one could check the operations.');
74
	}
75
76
	protected function execute(InputInterface $input, OutputInterface $output): int {
77
		if ($this->memoryLimit !== -1) {
78
			$limitInMiB = round($this->memoryLimit / 1024 /1024, 1);
79
			$thresholdInMiB = round($this->memoryTreshold / 1024 /1024, 1);
80
			$output->writeln("Memory limit is $limitInMiB MiB");
81
			$output->writeln("Memory threshold is $thresholdInMiB MiB");
82
			$output->writeln("");
83
			$memoryCheckEnabled = true;
84
		} else {
85
			$output->writeln("No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit.");
86
			$output->writeln("");
87
			$memoryCheckEnabled = false;
88
		}
89
90
		$dryMode = $input->getOption('dry');
91
92
		if ($dryMode) {
93
			$output->writeln("INFO: The migration is run in dry mode and will not modify anything.");
94
			$output->writeln("");
95
		}
96
97
		$verbose = $output->isVerbose();
98
99
		$instanceId = $this->config->getSystemValueString('instanceid');
100
101
		$output->writeln("This will migrate all previews from the old preview location to the new one.");
102
		$output->writeln('');
103
104
		$output->writeln('Fetching previews that need to be migrated …');
105
		/** @var \OCP\Files\Folder $currentPreviewFolder */
106
		$currentPreviewFolder = $this->rootFolder->get("appdata_$instanceId/preview");
107
108
		$directoryListing = $currentPreviewFolder->getDirectoryListing();
109
110
		$total = count($directoryListing);
111
		/**
112
		 * by default there could be 0-9 a-f and the old-multibucket folder which are all fine
113
		 */
114
		if ($total < 18) {
115
			$directoryListing = array_filter($directoryListing, function ($dir) {
116
				if ($dir->getName() === 'old-multibucket') {
117
					return false;
118
				}
119
120
				// a-f can't be a file ID -> removing from migration
121
				if (preg_match('!^[a-f]$!', $dir->getName())) {
122
					return false;
123
				}
124
125
				if (preg_match('!^[0-9]$!', $dir->getName())) {
126
					// ignore folders that only has folders in them
127
					if ($dir instanceof Folder) {
128
						foreach ($dir->getDirectoryListing() as $entry) {
129
							if (!$entry instanceof Folder) {
130
								return true;
131
							}
132
						}
133
						return false;
134
					}
135
				}
136
				return true;
137
			});
138
			$total = count($directoryListing);
139
		}
140
141
		if ($total === 0) {
142
			$output->writeln("All previews are already migrated.");
143
			return 0;
144
		}
145
146
		$output->writeln("A total of $total preview files need to be migrated.");
147
		$output->writeln("");
148
		$output->writeln("The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This fill finish the current batch and then stop the migration. This migration can then just be started and it will continue.");
149
150
		if ($input->getOption('batch')) {
151
			$output->writeln('Batch mode active: migration is started right away.');
152
		} else {
153
			$helper = $this->getHelper('question');
154
			$question = new ConfirmationQuestion('<info>Should the migration be started? (y/[n]) </info>', false);
155
156
			if (!$helper->ask($input, $output, $question)) {
157
				return 0;
158
			}
159
		}
160
161
		// register the SIGINT listener late in here to be able to exit in the early process of this command
162
		pcntl_signal(SIGINT, [$this, 'sigIntHandler']);
163
164
		$output->writeln("");
165
		$output->writeln("");
166
		$section1 = $output->section();
0 ignored issues
show
Bug introduced by
The method section() does not exist on Symfony\Component\Console\Output\OutputInterface. It seems like you code against a sub-type of Symfony\Component\Console\Output\OutputInterface such as Symfony\Component\Console\Style\OutputStyle or Symfony\Component\Console\Output\ConsoleOutput or Symfony\Component\Console\Output\ConsoleOutput. ( Ignorable by Annotation )

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

166
		/** @scrutinizer ignore-call */ 
167
  $section1 = $output->section();
Loading history...
167
		$section2 = $output->section();
168
		$progressBar = new ProgressBar($section2, $total);
169
		$progressBar->setFormat("%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%");
170
		$time = (new \DateTime())->format('H:i:s');
171
		$progressBar->setMessage("$time Starting …");
172
		$progressBar->maxSecondsBetweenRedraws(0.2);
173
		$progressBar->start();
174
175
		foreach ($directoryListing as $oldPreviewFolder) {
176
			pcntl_signal_dispatch();
177
			$name = $oldPreviewFolder->getName();
178
			$time = (new \DateTime())->format('H:i:s');
179
			$section1->writeln("$time Migrating previews of file with fileId $name …");
180
			$progressBar->display();
181
182
			if ($this->stopSignalReceived) {
183
				$section1->writeln("$time Stopping migration …");
184
				return 0;
185
			}
186
			if (!$oldPreviewFolder instanceof Folder) {
187
				$section1->writeln("         Skipping non-folder $name …");
188
				$progressBar->advance();
189
				continue;
190
			}
191
			if ($name === 'old-multibucket') {
192
				$section1->writeln("         Skipping fallback mount point $name …");
193
				$progressBar->advance();
194
				continue;
195
			}
196
			if (in_array($name, ['a', 'b', 'c', 'd', 'e', 'f'])) {
197
				$section1->writeln("         Skipping hex-digit folder $name …");
198
				$progressBar->advance();
199
				continue;
200
			}
201
			if (!preg_match('!^\d+$!', $name)) {
202
				$section1->writeln("         Skipping non-numeric folder $name …");
203
				$progressBar->advance();
204
				continue;
205
			}
206
207
			$newFoldername = Root::getInternalFolder($name);
208
209
			$memoryUsage = memory_get_usage();
210
			if ($memoryCheckEnabled && $memoryUsage > $this->memoryTreshold) {
211
				$section1->writeln("");
212
				$section1->writeln("");
213
				$section1->writeln("");
214
				$section1->writeln("         Stopped process 25 MB before reaching the memory limit to avoid a hard crash.");
215
				$time = (new \DateTime())->format('H:i:s');
216
				$section1->writeln("$time Reached memory limit and stopped to avoid hard crash.");
217
				return 1;
218
			}
219
220
			$previews = $oldPreviewFolder->getDirectoryListing();
221
			if ($previews !== []) {
222
				try {
223
					$this->rootFolder->get("appdata_$instanceId/preview/$newFoldername");
224
				} catch (NotFoundException $e) {
225
					if ($verbose) {
226
						$section1->writeln("         Create folder preview/$newFoldername");
227
					}
228
					if (!$dryMode) {
229
						$this->rootFolder->newFolder("appdata_$instanceId/preview/$newFoldername");
230
					}
231
				}
232
233
				foreach ($previews as $preview) {
234
					pcntl_signal_dispatch();
235
					$previewName = $preview->getName();
236
237
					if ($preview instanceof Folder) {
238
						$section1->writeln("         Skipping folder $name/$previewName …");
239
						$progressBar->advance();
240
						continue;
241
					}
242
					if ($verbose) {
243
						$section1->writeln("         Move preview/$name/$previewName to preview/$newFoldername");
244
					}
245
					if (!$dryMode) {
246
						try {
247
							$preview->move("appdata_$instanceId/preview/$newFoldername/$previewName");
248
						} catch (\Exception $e) {
249
							$this->logger->logException($e, ['app' => 'core', 'message' => "Failed to move preview from preview/$name/$previewName to preview/$newFoldername"]);
250
						}
251
					}
252
				}
253
			}
254
			if ($oldPreviewFolder->getDirectoryListing() === []) {
255
				if ($verbose) {
256
					$section1->writeln("         Delete empty folder preview/$name");
257
				}
258
				if (!$dryMode) {
259
					try {
260
						$oldPreviewFolder->delete();
261
					} catch (\Exception $e) {
262
						$this->logger->logException($e, ['app' => 'core', 'message' => "Failed to delete empty folder preview/$name"]);
263
					}
264
				}
265
			}
266
			$section1->writeln("         Finished migrating previews of file with fileId $name …");
267
			$progressBar->advance();
268
		}
269
270
		$progressBar->finish();
271
		$output->writeln("");
272
		return 0;
273
	}
274
275
	protected function sigIntHandler() {
276
		echo "\nSignal received - will finish the step and then stop the migration.\n\n\n";
277
		$this->stopSignalReceived = true;
278
	}
279
}
280