Passed
Push — master ( 8a3f8b...c798e9 )
by Joas
17:11 queued 13s
created

GenerateCommand::execute()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 46
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 30
c 1
b 0
f 0
nc 16
nop 2
dl 0
loc 46
rs 8.1954
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Joas Schilling <[email protected]>
4
 * @copyright Copyright (c) 2017, ownCloud GmbH
5
 *
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Roeland Jago Douma <[email protected]>
9
 *
10
 * @license AGPL-3.0
11
 *
12
 * This code is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License, version 3,
14
 * as published by the Free Software Foundation.
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, version 3,
22
 * along with this program. If not, see <http://www.gnu.org/licenses/>
23
 *
24
 */
25
namespace OC\Core\Command\Db\Migrations;
26
27
use OC\DB\Connection;
28
use OC\DB\MigrationService;
29
use OC\Migration\ConsoleOutput;
30
use OCP\App\IAppManager;
31
use OCP\Util;
32
use Stecman\Component\Symfony\Console\BashCompletion\Completion\CompletionAwareInterface;
33
use Stecman\Component\Symfony\Console\BashCompletion\CompletionContext;
34
use Symfony\Component\Console\Command\Command;
35
use Symfony\Component\Console\Exception\RuntimeException;
36
use Symfony\Component\Console\Input\InputArgument;
37
use Symfony\Component\Console\Input\InputInterface;
38
use Symfony\Component\Console\Output\OutputInterface;
39
use Symfony\Component\Console\Question\ConfirmationQuestion;
40
41
class GenerateCommand extends Command implements CompletionAwareInterface {
42
	protected static $_templateSimple =
43
		'<?php
44
45
declare(strict_types=1);
46
47
/**
48
 * @copyright Copyright (c) {{year}} Your name <[email protected]>
49
 *
50
 * @author Your name <[email protected]>
51
 *
52
 * @license GNU AGPL version 3 or any later version
53
 *
54
 * This program is free software: you can redistribute it and/or modify
55
 * it under the terms of the GNU Affero General Public License as
56
 * published by the Free Software Foundation, either version 3 of the
57
 * License, or (at your option) any later version.
58
 *
59
 * This program is distributed in the hope that it will be useful,
60
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
61
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
62
 * GNU Affero General Public License for more details.
63
 *
64
 * You should have received a copy of the GNU Affero General Public License
65
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
66
 *
67
 */
68
69
namespace {{namespace}};
70
71
use Closure;
72
use OCP\DB\ISchemaWrapper;
73
use OCP\Migration\IOutput;
74
use OCP\Migration\SimpleMigrationStep;
75
76
/**
77
 * Auto-generated migration step: Please modify to your needs!
78
 */
79
class {{classname}} extends SimpleMigrationStep {
80
81
	/**
82
	 * @param IOutput $output
83
	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
84
	 * @param array $options
85
	 */
86
	public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
87
	}
88
89
	/**
90
	 * @param IOutput $output
91
	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
92
	 * @param array $options
93
	 * @return null|ISchemaWrapper
94
	 */
95
	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
96
{{schemabody}}
97
	}
98
99
	/**
100
	 * @param IOutput $output
101
	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
102
	 * @param array $options
103
	 */
104
	public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
105
	}
106
}
107
';
108
109
	/** @var Connection */
110
	protected $connection;
111
112
	/** @var IAppManager */
113
	protected $appManager;
114
115
	/**
116
	 * @param Connection $connection
117
	 * @param IAppManager $appManager
118
	 */
119
	public function __construct(Connection $connection, IAppManager $appManager) {
120
		$this->connection = $connection;
121
		$this->appManager = $appManager;
122
123
		parent::__construct();
124
	}
125
126
	protected function configure() {
127
		$this
128
			->setName('migrations:generate')
129
			->addArgument('app', InputArgument::REQUIRED, 'Name of the app this migration command shall work on')
130
			->addArgument('version', InputArgument::REQUIRED, 'Major version of this app, to allow versions on parallel development branches')
131
		;
132
133
		parent::configure();
134
	}
135
136
	public function execute(InputInterface $input, OutputInterface $output): int {
137
		$appName = $input->getArgument('app');
138
		$version = $input->getArgument('version');
139
140
		if (!preg_match('/^\d{1,16}$/', $version)) {
141
			$output->writeln('<error>The given version is invalid. Only 0-9 are allowed (max. 16 digits)</error>');
142
			return 1;
143
		}
144
145
		if ($appName === 'core') {
146
			$fullVersion = implode('.', Util::getVersion());
147
		} else {
148
			try {
149
				$fullVersion = $this->appManager->getAppVersion($appName, false);
150
			} catch (\Throwable $e) {
151
				$fullVersion = '';
152
			}
153
		}
154
155
		if ($fullVersion) {
156
			[$major, $minor] = explode('.', $fullVersion);
157
			$shouldVersion = (int)$major * 1000 + (int)$minor;
158
			if ($version !== $shouldVersion) {
159
				$output->writeln('<comment>Unexpected migration version for current version: ' . $fullVersion . '</comment>');
160
				$output->writeln('<comment> - Pattern:  XYYY </comment>');
161
				$output->writeln('<comment> - Expected: ' . $shouldVersion . '</comment>');
162
				$output->writeln('<comment> - Actual:   ' . $version . '</comment>');
163
164
				if ($input->isInteractive()) {
165
					$helper = $this->getHelper('question');
166
					$question = new ConfirmationQuestion('Continue with your given version? (y/n) [n] ', false);
167
168
					if (!$helper->ask($input, $output, $question)) {
169
						return 1;
170
					}
171
				}
172
			}
173
		}
174
175
		$ms = new MigrationService($appName, $this->connection, new ConsoleOutput($output));
176
177
		$date = date('YmdHis');
178
		$path = $this->generateMigration($ms, 'Version' . $version . 'Date' . $date);
179
180
		$output->writeln("New migration class has been generated to <info>$path</info>");
181
		return 0;
182
	}
183
184
	/**
185
	 * @param string $optionName
186
	 * @param CompletionContext $context
187
	 * @return string[]
188
	 */
189
	public function completeOptionValues($optionName, CompletionContext $context) {
190
		return [];
191
	}
192
193
	/**
194
	 * @param string $argumentName
195
	 * @param CompletionContext $context
196
	 * @return string[]
197
	 */
198
	public function completeArgumentValues($argumentName, CompletionContext $context) {
199
		if ($argumentName === 'app') {
200
			$allApps = \OC_App::getAllApps();
201
			return array_diff($allApps, \OC_App::getEnabledApps(true, true));
202
		}
203
204
		if ($argumentName === 'version') {
205
			$appName = $context->getWordAtIndex($context->getWordIndex() - 1);
206
207
			$version = explode('.', $this->appManager->getAppVersion($appName));
208
			return [$version[0] . sprintf('%1$03d', $version[1])];
209
		}
210
211
		return [];
212
	}
213
214
	/**
215
	 * @param MigrationService $ms
216
	 * @param string $className
217
	 * @param string $schemaBody
218
	 * @return string
219
	 */
220
	protected function generateMigration(MigrationService $ms, $className, $schemaBody = '') {
221
		if ($schemaBody === '') {
222
			$schemaBody = "\t\t" . 'return null;';
223
		}
224
225
226
		$placeHolders = [
227
			'{{namespace}}',
228
			'{{classname}}',
229
			'{{schemabody}}',
230
			'{{year}}',
231
		];
232
		$replacements = [
233
			$ms->getMigrationsNamespace(),
234
			$className,
235
			$schemaBody,
236
			date('Y')
237
		];
238
		$code = str_replace($placeHolders, $replacements, self::$_templateSimple);
239
		$dir = $ms->getMigrationsDirectory();
240
241
		$this->ensureMigrationDirExists($dir);
242
		$path = $dir . '/' . $className . '.php';
243
244
		if (file_put_contents($path, $code) === false) {
245
			throw new RuntimeException('Failed to generate new migration step.');
246
		}
247
248
		return $path;
249
	}
250
251
	protected function ensureMigrationDirExists($directory) {
252
		if (file_exists($directory) && is_dir($directory)) {
253
			return;
254
		}
255
256
		if (file_exists($directory)) {
257
			throw new \RuntimeException("Could not create folder \"$directory\"");
258
		}
259
260
		$this->ensureMigrationDirExists(dirname($directory));
261
262
		if (!@mkdir($directory) && !is_dir($directory)) {
263
			throw new \RuntimeException("Could not create folder \"$directory\"");
264
		}
265
	}
266
}
267