Passed
Push — master ( 37146c...328fbd )
by Roeland
18:50 queued 08:45
created

OwnershipTransferService::transfer()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 86
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 48
nc 10
nop 6
dl 0
loc 86
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright 2019 Christoph Wurst <[email protected]>
7
 *
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Roeland Jago Douma <[email protected]>
10
 * @author Sascha Wiswedel <[email protected]>
11
 * @author Tobia De Koninck <[email protected]>
12
 *
13
 * @license GNU AGPL version 3 or any later version
14
 *
15
 * This program is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License as
17
 * published by the Free Software Foundation, either version 3 of the
18
 * License, or (at your option) any later version.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27
 *
28
 */
29
30
namespace OCA\Files\Service;
31
32
use Closure;
33
use OC\Files\Filesystem;
34
use OC\Files\View;
35
use OCA\Files\Exception\TransferOwnershipException;
36
use OCP\Encryption\IManager as IEncryptionManager;
37
use OCP\Files\FileInfo;
38
use OCP\Files\IHomeStorage;
39
use OCP\Files\InvalidPathException;
40
use OCP\Files\Mount\IMountManager;
41
use OCP\IUser;
42
use OCP\Share\IManager as IShareManager;
43
use Symfony\Component\Console\Helper\ProgressBar;
44
use Symfony\Component\Console\Output\NullOutput;
45
use Symfony\Component\Console\Output\OutputInterface;
46
use function array_merge;
47
use function basename;
48
use function count;
49
use function date;
50
use function is_dir;
51
use function rtrim;
52
53
class OwnershipTransferService {
54
55
	/** @var IEncryptionManager */
56
	private $encryptionManager;
57
58
	/** @var IShareManager */
59
	private $shareManager;
60
61
	/** @var IMountManager */
62
	private $mountManager;
63
64
	public function __construct(IEncryptionManager $manager,
65
								IShareManager $shareManager,
66
								IMountManager $mountManager) {
67
		$this->encryptionManager = $manager;
68
		$this->shareManager = $shareManager;
69
		$this->mountManager = $mountManager;
70
	}
71
72
	/**
73
	 * @param IUser $sourceUser
74
	 * @param IUser $destinationUser
75
	 * @param string $path
76
	 *
77
	 * @param OutputInterface|null $output
78
	 * @param bool $move
79
	 * @throws TransferOwnershipException
80
	 * @throws \OC\User\NoUserException
81
	 */
82
	public function transfer(IUser $sourceUser,
83
							 IUser $destinationUser,
84
							 string $path,
85
							 ?OutputInterface $output = null,
86
							 bool $move = false,
87
							 bool $firstLogin = false): void {
88
		$output = $output ?? new NullOutput();
89
		$sourceUid = $sourceUser->getUID();
90
		$destinationUid = $destinationUser->getUID();
91
		$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
92
93
		// target user has to be ready
94
		if ($destinationUser->getLastLogin() === 0 || !$this->encryptionManager->isReadyForUser($destinationUid)) {
0 ignored issues
show
Bug introduced by
The method isReadyForUser() does not exist on OCP\Encryption\IManager. Since it exists in all sub-types, consider adding an abstract or default implementation to OCP\Encryption\IManager. ( Ignorable by Annotation )

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

94
		if ($destinationUser->getLastLogin() === 0 || !$this->encryptionManager->/** @scrutinizer ignore-call */ isReadyForUser($destinationUid)) {
Loading history...
95
			throw new TransferOwnershipException("The target user is not ready to accept files. The user has at least to have logged in once.", 2);
96
		}
97
98
		// setup filesystem
99
		Filesystem::initMountPoints($sourceUid);
100
		Filesystem::initMountPoints($destinationUid);
101
102
		$view = new View();
103
104
		if ($move) {
105
			$finalTarget = "$destinationUid/files/";
106
		} else {
107
			$date = date('Y-m-d H-i-s');
108
109
			// Remove some characters which are prone to cause errors
110
			$cleanUserName = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $sourceUser->getDisplayName());
111
			// Replace multiple dashes with one dash
112
			$cleanUserName = preg_replace('/-{2,}/s', '-', $cleanUserName);
113
			$cleanUserName = $cleanUserName ?: $sourceUid;
114
115
			$finalTarget = "$destinationUid/files/transferred from $cleanUserName on $date";
116
			try {
117
				$view->verifyPath(dirname($finalTarget), basename($finalTarget));
118
			} catch (InvalidPathException $e) {
119
				$finalTarget = "$destinationUid/files/transferred from $sourceUid on $date";
120
			}
121
		}
122
123
		if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
124
			throw new TransferOwnershipException("Unknown path provided: $path", 1);
125
		}
126
127
		if ($move && (
128
				!$view->is_dir($finalTarget) || (
129
					!$firstLogin &&
130
					count($view->getDirectoryContent($finalTarget)) > 0
131
				)
132
			)
133
		) {
134
			throw new TransferOwnershipException("Destination path does not exists or is not empty", 1);
135
		}
136
137
138
		// analyse source folder
139
		$this->analyse(
140
			$sourceUid,
141
			$destinationUid,
142
			$sourcePath,
143
			$view,
144
			$output
145
		);
146
147
		// collect all the shares
148
		$shares = $this->collectUsersShares(
149
			$sourceUid,
150
			$output
151
		);
152
153
		// transfer the files
154
		$this->transferFiles(
155
			$sourceUid,
156
			$sourcePath,
157
			$finalTarget,
158
			$view,
159
			$output
160
		);
161
162
		// restore the shares
163
		$this->restoreShares(
164
			$sourceUid,
165
			$destinationUid,
166
			$shares,
167
			$output
168
		);
169
	}
170
171
	private function walkFiles(View $view, $path, Closure $callBack) {
172
		foreach ($view->getDirectoryContent($path) as $fileInfo) {
173
			if (!$callBack($fileInfo)) {
174
				return;
175
			}
176
			if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
177
				$this->walkFiles($view, $fileInfo->getPath(), $callBack);
178
			}
179
		}
180
	}
181
182
	/**
183
	 * @param OutputInterface $output
184
	 *
185
	 * @throws \Exception
186
	 */
187
	protected function analyse(string $sourceUid,
188
							   string $destinationUid,
189
							   string $sourcePath,
190
							   View $view,
191
							   OutputInterface $output): void {
192
		$output->writeln('Validating quota');
193
		$size = $view->getFileInfo($sourcePath, false)->getSize(false);
194
		$freeSpace = $view->free_space($destinationUid . '/files/');
195
		if ($size > $freeSpace) {
196
			$output->writeln('<error>Target user does not have enough free space available.</error>');
197
			throw new \Exception('Execution terminated.');
198
		}
199
200
		$output->writeln("Analysing files of $sourceUid ...");
201
		$progress = new ProgressBar($output);
202
		$progress->start();
203
204
		$encryptedFiles = [];
205
		$this->walkFiles($view, $sourcePath,
206
			function (FileInfo $fileInfo) use ($progress) {
207
				if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
208
					// only analyze into folders from main storage,
209
					if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) {
210
						return false;
211
					}
212
					return true;
213
				}
214
				$progress->advance();
215
				if ($fileInfo->isEncrypted()) {
216
					$encryptedFiles[] = $fileInfo;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$encryptedFiles was never initialized. Although not strictly required by PHP, it is generally a good practice to add $encryptedFiles = array(); before regardless.
Loading history...
217
				}
218
				return true;
219
			});
220
		$progress->finish();
221
		$output->writeln('');
222
223
		// no file is allowed to be encrypted
224
		if (!empty($encryptedFiles)) {
225
			$output->writeln("<error>Some files are encrypted - please decrypt them first.</error>");
226
			foreach ($encryptedFiles as $encryptedFile) {
227
				/** @var FileInfo $encryptedFile */
228
				$output->writeln("  " . $encryptedFile->getPath());
229
			}
230
			throw new \Exception('Execution terminated.');
231
		}
232
	}
233
234
	private function collectUsersShares(string $sourceUid,
235
										OutputInterface $output): array {
236
		$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
237
238
		$shares = [];
239
		$progress = new ProgressBar($output);
240
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {
0 ignored issues
show
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_REMOTE has been deprecated: 17.0.0 - use IShare::TYPE_REMOTE instead ( Ignorable by Annotation )

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

240
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, /** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_LINK has been deprecated: 17.0.0 - use IShare::TYPE_LINK instead ( Ignorable by Annotation )

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

240
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, /** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_ROOM has been deprecated: 17.0.0 - use IShare::TYPE_ROOM instead ( Ignorable by Annotation )

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

240
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, /** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_GROUP has been deprecated: 17.0.0 - use IShare::TYPE_GROUP instead ( Ignorable by Annotation )

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

240
		foreach ([/** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_USER has been deprecated: 17.0.0 - use IShare::TYPE_USER instead ( Ignorable by Annotation )

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

240
		foreach ([\OCP\Share::SHARE_TYPE_GROUP, /** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE, \OCP\Share::SHARE_TYPE_ROOM] as $shareType) {

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
241
			$offset = 0;
242
			while (true) {
243
				$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset);
244
				$progress->advance(count($sharePage));
245
				if (empty($sharePage)) {
246
					break;
247
				}
248
				$shares = array_merge($shares, $sharePage);
249
				$offset += 50;
250
			}
251
		}
252
253
		$progress->finish();
254
		$output->writeln('');
255
		return $shares;
256
	}
257
258
	/**
259
	 * @throws TransferOwnershipException
260
	 */
261
	protected function transferFiles(string $sourceUid,
262
									 string $sourcePath,
263
									 string $finalTarget,
264
									 View $view,
265
									 OutputInterface $output): void {
266
		$output->writeln("Transferring files to $finalTarget ...");
267
268
		// This change will help user to transfer the folder specified using --path option.
269
		// Else only the content inside folder is transferred which is not correct.
270
		if ($sourcePath !== "$sourceUid/files") {
271
			$view->mkdir($finalTarget);
272
			$finalTarget = $finalTarget . '/' . basename($sourcePath);
273
		}
274
		if ($view->rename($sourcePath, $finalTarget) === false) {
275
			throw new TransferOwnershipException("Could not transfer files.", 1);
276
		}
277
		if (!is_dir("$sourceUid/files")) {
278
			// because the files folder is moved away we need to recreate it
279
			$view->mkdir("$sourceUid/files");
280
		}
281
	}
282
283
	private function restoreShares(string $sourceUid,
284
								   string $destinationUid,
285
								   array $shares,
286
								   OutputInterface $output) {
287
		$output->writeln("Restoring shares ...");
288
		$progress = new ProgressBar($output, count($shares));
289
290
		foreach ($shares as $share) {
291
			try {
292
				if ($share->getShareType() === \OCP\Share::SHARE_TYPE_USER &&
0 ignored issues
show
Deprecated Code introduced by
The constant OC\Share\Constants::SHARE_TYPE_USER has been deprecated: 17.0.0 - use IShare::TYPE_USER instead ( Ignorable by Annotation )

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

292
				if ($share->getShareType() === /** @scrutinizer ignore-deprecated */ \OCP\Share::SHARE_TYPE_USER &&

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
293
					$share->getSharedWith() === $destinationUid) {
294
					// Unmount the shares before deleting, so we don't try to get the storage later on.
295
					$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
296
					if ($shareMountPoint) {
297
						$this->mountManager->removeMount($shareMountPoint->getMountPoint());
298
					}
299
					$this->shareManager->deleteShare($share);
300
				} else {
301
					if ($share->getShareOwner() === $sourceUid) {
302
						$share->setShareOwner($destinationUid);
303
					}
304
					if ($share->getSharedBy() === $sourceUid) {
305
						$share->setSharedBy($destinationUid);
306
					}
307
308
					$this->shareManager->updateShare($share);
309
				}
310
			} catch (\OCP\Files\NotFoundException $e) {
311
				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
312
			} catch (\Throwable $e) {
313
				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
314
			}
315
			$progress->advance();
316
		}
317
		$progress->finish();
318
		$output->writeln('');
319
	}
320
321
}
322