Completed
Push — master ( 0e6e62...37fbc3 )
by
unknown
29:06 queued 28:40
created

TransferOwnership::restoreShares()   B

Complexity

Conditions 7
Paths 24

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 24
nop 1
dl 0
loc 46
rs 8.2448
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Carla Schroder <[email protected]>
4
 * @author Joas Schilling <[email protected]>
5
 * @author Thomas Müller <[email protected]>
6
 * @author Vincent Petry <[email protected]>
7
 *
8
 * @copyright Copyright (c) 2018, ownCloud GmbH
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OCA\Files\Command;
26
27
use OC\Encryption\Manager;
28
use OC\Files\Filesystem;
29
use OC\Files\View;
30
use OC\Share20\ProviderFactory;
31
use OCP\Files\FileInfo;
32
use OCP\Files\Mount\IMountManager;
33
use OCP\ILogger;
34
use OCP\IUserManager;
35
use OCP\Share\IManager;
36
use OCP\Share\IShare;
37
use Symfony\Component\Console\Command\Command;
38
use Symfony\Component\Console\Helper\ProgressBar;
39
use Symfony\Component\Console\Input\InputArgument;
40
use Symfony\Component\Console\Input\InputInterface;
41
use Symfony\Component\Console\Input\InputOption;
42
use Symfony\Component\Console\Output\OutputInterface;
43
44
class TransferOwnership extends Command {
45
46
	/** @var IUserManager $userManager */
47
	private $userManager;
48
49
	/** @var IManager */
50
	private $shareManager;
51
52
	/** @var IMountManager */
53
	private $mountManager;
54
55
	/** @var Manager  */
56
	private $encryptionManager;
57
58
	/** @var ILogger  */
59
	private $logger;
60
61
	/** @var ProviderFactory  */
62
	private $shareProviderFactory;
63
64
	/** @var bool */
65
	private $filesExist = false;
66
67
	/** @var bool */
68
	private $foldersExist = false;
69
70
	/** @var FileInfo[] */
71
	private $encryptedFiles = [];
72
73
	/** @var IShare[] */
74
	private $shares = [];
75
76
	/** @var string */
77
	private $sourceUser;
78
79
	/** @var string */
80
	private $destinationUser;
81
82
	/** @var string */
83
	private $inputPath;
84
85
	/** @var string */
86
	private $finalTarget;
87
88
	public function __construct(IUserManager $userManager, IManager $shareManager, IMountManager $mountManager, Manager $encryptionManager, ILogger $logger, ProviderFactory $shareProviderFactory) {
89
		$this->userManager = $userManager;
90
		$this->shareManager = $shareManager;
91
		$this->mountManager = $mountManager;
92
		$this->encryptionManager = $encryptionManager;
93
		$this->logger = $logger;
94
		$this->shareProviderFactory = $shareProviderFactory;
95
		parent::__construct();
96
	}
97
98
	protected function configure() {
99
		$this
100
			->setName('files:transfer-ownership')
101
			->setDescription('All files and folders are moved to another user - shares are moved as well.')
102
			->addArgument(
103
				'source-user',
104
				InputArgument::REQUIRED,
105
				'owner of files which shall be moved'
106
			)
107
			->addArgument(
108
				'destination-user',
109
				InputArgument::REQUIRED,
110
				'user who will be the new owner of the files'
111
			)
112
			->addOption(
113
				'path',
114
				null,
115
				InputOption::VALUE_REQUIRED,
116
				'selectively provide the path to transfer. For example --path="folder_name"'
117
			);
118
	}
119
120
	protected function execute(InputInterface $input, OutputInterface $output) {
121
		$sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
122
		$destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
123
		if ($sourceUserObject === null) {
124
			$output->writeln("<error>Unknown source user $this->sourceUser</error>");
125
			return 1;
126
		}
127
		if ($destinationUserObject === null) {
128
			$output->writeln("<error>Unknown destination user $this->destinationUser</error>");
129
			return 1;
130
		}
131
132
		$this->sourceUser = $sourceUserObject->getUID();
133
		$this->destinationUser = $destinationUserObject->getUID();
134
		$this->inputPath = $input->getOption('path');
135
		$this->inputPath = \ltrim($this->inputPath, '/');
136
137
		// target user has to be ready
138
		if (!\OC::$server->getEncryptionManager()->isReadyForUser($this->destinationUser)) {
139
			$output->writeln("<error>The target user is not ready to accept files. The user has at least to be logged in once.</error>");
140
			return 2;
141
		}
142
143
		// use a date format compatible across client OS
144
		$date = \date('Ymd_his');
145
		$this->finalTarget = "$this->destinationUser/files/transferred from $this->sourceUser on $date";
146
147
		// setup filesystem
148
		Filesystem::initMountPoints($this->sourceUser);
149
		Filesystem::initMountPoints($this->destinationUser);
150
151
		if (\strlen($this->inputPath) >= 1) {
152
			$view = new View();
153
			$unknownDir = $this->inputPath;
154
			$this->inputPath = $this->sourceUser . "/files/" . $this->inputPath;
155
			if (!$view->is_dir($this->inputPath)) {
156
				$output->writeln("<error>Unknown path provided: $unknownDir</error>");
157
				return 1;
158
			}
159
		}
160
161
		// analyse source folder
162
		$this->analyse($output);
163
164
		if (!$this->filesExist and !$this->foldersExist) {
165
			$output->writeln("<comment>No files/folders to transfer</comment>");
166
			return 1;
167
		}
168
169
		// collect all the shares
170
		$this->collectUsersShares($output);
171
172
		// transfer the files
173
		$this->transfer($output);
174
175
		// restore the shares
176
		return $this->restoreShares($output);
177
	}
178
179
	private function walkFiles(View $view, $path, \Closure $callBack) {
180
		foreach ($view->getDirectoryContent($path) as $fileInfo) {
181
			if (!$callBack($fileInfo)) {
182
				return;
183
			}
184
			if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
185
				$this->walkFiles($view, $fileInfo->getPath(), $callBack);
186
			}
187
		}
188
	}
189
190
	/**
191
	 * @param OutputInterface $output
192
	 * @throws \Exception
193
	 */
194
	protected function analyse(OutputInterface $output) {
195
		$view = new View();
196
		$output->writeln("Analysing files of $this->sourceUser ...");
197
		$progress = new ProgressBar($output);
198
		$progress->start();
199
		$self = $this;
200
		$walkPath = "$this->sourceUser/files";
201
		if (\strlen($this->inputPath) > 0) {
202
			if ($this->inputPath !== "$this->sourceUser/files") {
203
				$walkPath = $this->inputPath;
204
				$this->foldersExist = true;
205
			}
206
		}
207
208
		$this->walkFiles($view, $walkPath,
209
				function (FileInfo $fileInfo) use ($progress, $self) {
210
					if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
211
						// only analyze into folders from main storage,
212
						// sub-storages have an empty internal path
213
						if ($fileInfo->getInternalPath() === '' && $fileInfo->getPath() !== '') {
214
							return false;
215
						}
216
217
						$this->foldersExist = true;
218
						return true;
219
					}
220
					$progress->advance();
221
					$this->filesExist = true;
222 View Code Duplication
					if ($fileInfo->isEncrypted()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223
						if (\OC::$server->getAppConfig()->getValue('encryption', 'useMasterKey', 0) !== 0) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of \OC::$server->getAppConf...on', 'useMasterKey', 0) (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
224
							/**
225
							 * We are not going to add this to encryptedFiles array.
226
							 * Because its encrypted with masterKey and hence it doesn't
227
							 * require user's specific password.
228
							 */
229
							return true;
230
						}
231
						$this->encryptedFiles[] = $fileInfo;
232
					}
233
					return true;
234
				});
235
		$progress->finish();
236
		$output->writeln('');
237
238
		// no file is allowed to be encrypted
239
		if (!empty($this->encryptedFiles)) {
240
			$output->writeln("<error>Some files are encrypted - please decrypt them first</error>");
241
			foreach ($this->encryptedFiles as $encryptedFile) {
242
				/** @var FileInfo $encryptedFile */
243
				$output->writeln("  " . $encryptedFile->getPath());
244
			}
245
			throw new \Exception('Execution terminated.');
246
		}
247
	}
248
249
	/**
250
	 * @param OutputInterface $output
251
	 */
252
	private function collectUsersShares(OutputInterface $output) {
253
		$output->writeln("Collecting all share information for files and folder of $this->sourceUser ...");
254
255
		$progress = new ProgressBar($output, \count($this->shares));
256
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE] as $shareType) {
257
			$offset = 0;
258
			while (true) {
259
				$sharePage = $this->shareManager->getSharesBy($this->sourceUser, $shareType, null, true, 50, $offset);
260
				$progress->advance(\count($sharePage));
261
				if (empty($sharePage)) {
262
					break;
263
				}
264
265
				$this->shares = \array_merge($this->shares, $sharePage);
266
				$offset += 50;
267
			}
268
		}
269
270
		$progress->finish();
271
		$output->writeln('');
272
	}
273
274
	/**
275
	 * @param OutputInterface $output
276
	 */
277
	protected function transfer(OutputInterface $output) {
278
		$view = new View();
279
		$output->writeln("Transferring files to $this->finalTarget ...");
280
		$sourcePath = (\strlen($this->inputPath) > 0) ? $this->inputPath : "$this->sourceUser/files";
281
		// This change will help user to transfer the folder specified using --path option.
282
		// Else only the content inside folder is transferred which is not correct.
283
		if (\strlen($this->inputPath) > 0) {
284
			if ($this->inputPath !== \ltrim("$this->sourceUser/files", '/')) {
285
				$view->mkdir($this->finalTarget);
286
				$this->finalTarget = $this->finalTarget . '/' . \basename($sourcePath);
287
			}
288
		}
289
290
		$view->rename($sourcePath, $this->finalTarget);
291
292
		if (!\is_dir("$this->sourceUser/files")) {
293
			// because the files folder is moved away we need to recreate it
294
			$view->mkdir("$this->sourceUser/files");
295
		}
296
	}
297
298
	/**
299
	 * @param OutputInterface $output
300
	 */
301
	private function restoreShares(OutputInterface $output) {
302
		$output->writeln("Restoring shares ...");
303
		$progress = new ProgressBar($output, \count($this->shares));
304
		$status = 0;
305
306
		$childShares = [];
307
		foreach ($this->shares as $share) {
308
			try {
309
				/**
310
				 * Do not process children which are already processed.
311
				 * This piece of code populates the childShare array
312
				 * with the child shares which will be processed. And
313
				 * hence will avoid further processing of same share
314
				 * again.
315
				 */
316
				if ($share->getSharedWith() === $this->destinationUser) {
317
					$provider = $this->shareProviderFactory->getProviderForType($share->getShareType());
318
					foreach ($provider->getChildren($share) as $child) {
319
						$childShares[] = $child->getId();
320
					}
321
				} else {
322
					/**
323
					 * Before doing handover to transferShare, check if the share
324
					 * id is present in the childShares. If so then just ignore
325
					 * this share and continue. If not ignored, the child shares
326
					 * would be processed again, if their parent share was shared
327
					 * with destination user. And hence we can safely avoid the
328
					 * duplicate processing of shares here.
329
					 */
330
					if (\in_array($share->getId(), $childShares, true)) {
331
						continue;
332
					}
333
				}
334
				$this->shareManager->transferShare($share, $this->sourceUser, $this->destinationUser, $this->finalTarget);
335
			} catch (\OCP\Files\NotFoundException $e) {
336
				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
337
			} catch (\Exception $e) {
338
				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
339
				$status = 1;
340
			}
341
			$progress->advance();
342
		}
343
		$progress->finish();
344
		$output->writeln('');
345
		return $status;
346
	}
347
}
348