Passed
Push — master ( 701550...dfe85a )
by Roeland
16:47 queued 13s
created

OwnershipTransferService::collectUsersShares()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 15
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 22
rs 9.7666
1
<?php declare(strict_types=1);
2
3
/**
4
 * @copyright 2019 Christoph Wurst <[email protected]>
5
 *
6
 * @author 2019 Christoph Wurst <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
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
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
namespace OCA\Files\Service;
25
26
use Closure;
27
use OC\Files\Filesystem;
28
use OC\Files\View;
29
use OCA\Files\Exception\TransferOwnershipException;
30
use OCP\Encryption\IManager as IEncryptionManager;
31
use OCP\Files\FileInfo;
32
use OCP\Files\IHomeStorage;
33
use OCP\Files\Mount\IMountManager;
34
use OCP\IUser;
35
use OCP\Share\IManager as IShareManager;
36
use Symfony\Component\Console\Helper\ProgressBar;
37
use Symfony\Component\Console\Output\NullOutput;
38
use Symfony\Component\Console\Output\OutputInterface;
39
use function array_merge;
40
use function basename;
41
use function count;
42
use function date;
43
use function is_dir;
44
use function rtrim;
45
46
class OwnershipTransferService {
47
48
	/** @var IEncryptionManager */
49
	private $encryptionManager;
50
51
	/** @var IShareManager */
52
	private $shareManager;
53
54
	/** @var IMountManager */
55
	private $mountManager;
56
57
	public function __construct(IEncryptionManager $manager,
58
								IShareManager $shareManager,
59
								IMountManager $mountManager) {
60
		$this->encryptionManager = $manager;
61
		$this->shareManager = $shareManager;
62
		$this->mountManager = $mountManager;
63
	}
64
65
	/**
66
	 * @param IUser $sourceUser
67
	 * @param IUser $destinationUser
68
	 * @param string $path
69
	 *
70
	 * @throws TransferOwnershipException
71
	 */
72
	public function transfer(IUser $sourceUser,
73
							 IUser $destinationUser,
74
							 string $path,
75
							 ?OutputInterface $output = null): void {
76
		$output = $output ?? new NullOutput();
77
		$sourceUid = $sourceUser->getUID();
78
		$destinationUid = $destinationUser->getUID();
79
		$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
80
81
		// target user has to be ready
82
		if (!$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

82
		if (!$this->encryptionManager->/** @scrutinizer ignore-call */ isReadyForUser($destinationUid)) {
Loading history...
83
			throw new TransferOwnershipException("The target user is not ready to accept files. The user has at least to be logged in once.", 2);
84
		}
85
86
		$date = date('Y-m-d H-i-s');
87
		$finalTarget = "$destinationUid/files/transferred from $sourceUid on $date";
88
89
		// setup filesystem
90
		Filesystem::initMountPoints($sourceUid);
91
		Filesystem::initMountPoints($destinationUid);
92
93
		$view = new View();
94
		if (!$view->is_dir($sourcePath)) {
95
			throw new TransferOwnershipException("Unknown path provided: $path", 1);
96
		}
97
98
		// analyse source folder
99
		$this->analyse(
100
			$sourceUid,
101
			$destinationUid,
102
			$sourcePath,
103
			$view,
104
			$output
105
		);
106
107
		// collect all the shares
108
		$shares = $this->collectUsersShares(
109
			$sourceUid,
110
			$output
111
		);
112
113
		// transfer the files
114
		$this->transferFiles(
115
			$sourceUid,
116
			$sourcePath,
117
			$finalTarget,
118
			$view,
119
			$output
120
		);
121
122
		// restore the shares
123
		$this->restoreShares(
124
			$sourceUid,
125
			$destinationUid,
126
			$shares,
127
			$output
128
		);
129
	}
130
131
	private function walkFiles(View $view, $path, Closure $callBack) {
132
		foreach ($view->getDirectoryContent($path) as $fileInfo) {
133
			if (!$callBack($fileInfo)) {
134
				return;
135
			}
136
			if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
137
				$this->walkFiles($view, $fileInfo->getPath(), $callBack);
138
			}
139
		}
140
	}
141
142
	/**
143
	 * @param OutputInterface $output
144
	 *
145
	 * @throws \Exception
146
	 */
147
	protected function analyse(string $sourceUid,
148
							   string $destinationUid,
149
							   string $sourcePath,
150
							   View $view,
151
							   OutputInterface $output): void {
152
		$output->writeln('Validating quota');
153
		$size = $view->getFileInfo($sourcePath, false)->getSize(false);
154
		$freeSpace = $view->free_space($destinationUid . '/files/');
155
		if ($size > $freeSpace) {
156
			$output->writeln('<error>Target user does not have enough free space available</error>');
157
			throw new \Exception('Execution terminated');
158
		}
159
160
		$output->writeln("Analysing files of $sourceUid ...");
161
		$progress = new ProgressBar($output);
162
		$progress->start();
163
164
		$encryptedFiles = [];
165
		$this->walkFiles($view, $sourcePath,
166
			function (FileInfo $fileInfo) use ($progress) {
167
				if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
168
					// only analyze into folders from main storage,
169
					if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) {
170
						return false;
171
					}
172
					return true;
173
				}
174
				$progress->advance();
175
				if ($fileInfo->isEncrypted()) {
176
					$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...
177
				}
178
				return true;
179
			});
180
		$progress->finish();
181
		$output->writeln('');
182
183
		// no file is allowed to be encrypted
184
		if (!empty($encryptedFiles)) {
185
			$output->writeln("<error>Some files are encrypted - please decrypt them first</error>");
186
			foreach ($encryptedFiles as $encryptedFile) {
187
				/** @var FileInfo $encryptedFile */
188
				$output->writeln("  " . $encryptedFile->getPath());
189
			}
190
			throw new \Exception('Execution terminated.');
191
		}
192
	}
193
194
	private function collectUsersShares(string $sourceUid,
195
										OutputInterface $output): array {
196
		$output->writeln("Collecting all share information for files and folder of $sourceUid ...");
197
198
		$shares = [];
199
		$progress = new ProgressBar($output);
200
		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_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

200
		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...
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

200
		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_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

200
		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_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

200
		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_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

200
		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...
201
			$offset = 0;
202
			while (true) {
203
				$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset);
204
				$progress->advance(count($sharePage));
205
				if (empty($sharePage)) {
206
					break;
207
				}
208
				$shares = array_merge($shares, $sharePage);
209
				$offset += 50;
210
			}
211
		}
212
213
		$progress->finish();
214
		$output->writeln('');
215
		return $shares;
216
	}
217
218
	protected function transferFiles(string $sourceUid,
219
									 string $sourcePath,
220
									 string $finalTarget,
221
									 View $view,
222
									 OutputInterface $output): void {
223
		$output->writeln("Transferring files to $finalTarget ...");
224
225
		// This change will help user to transfer the folder specified using --path option.
226
		// Else only the content inside folder is transferred which is not correct.
227
		if ($sourcePath !== "$sourceUid/files") {
228
			$view->mkdir($finalTarget);
229
			$finalTarget = $finalTarget . '/' . basename($sourcePath);
230
		}
231
		$view->rename($sourcePath, $finalTarget);
232
		if (!is_dir("$sourceUid/files")) {
233
			// because the files folder is moved away we need to recreate it
234
			$view->mkdir("$sourceUid/files");
235
		}
236
	}
237
238
	private function restoreShares(string $sourceUid,
239
								   string $destinationUid,
240
								   array $shares,
241
								   OutputInterface $output) {
242
		$output->writeln("Restoring shares ...");
243
		$progress = new ProgressBar($output, count($shares));
244
245
		foreach ($shares as $share) {
246
			try {
247
				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

247
				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...
248
					$share->getSharedWith() === $destinationUid) {
249
					// Unmount the shares before deleting, so we don't try to get the storage later on.
250
					$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
251
					if ($shareMountPoint) {
252
						$this->mountManager->removeMount($shareMountPoint->getMountPoint());
253
					}
254
					$this->shareManager->deleteShare($share);
255
				} else {
256
					if ($share->getShareOwner() === $sourceUid) {
257
						$share->setShareOwner($destinationUid);
258
					}
259
					if ($share->getSharedBy() === $sourceUid) {
260
						$share->setSharedBy($destinationUid);
261
					}
262
263
					$this->shareManager->updateShare($share);
264
				}
265
			} catch (\OCP\Files\NotFoundException $e) {
266
				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
267
			} catch (\Exception $e) {
268
				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
269
			}
270
			$progress->advance();
271
		}
272
		$progress->finish();
273
		$output->writeln('');
274
	}
275
276
}
277