Completed
Push — master ( 5bf67f...86f0cb )
by
unknown
28:56
created
apps/files/lib/Command/TransferOwnership.php 2 patches
Indentation   +116 added lines, -116 removed lines patch added patch discarded remove patch
@@ -26,130 +26,130 @@
 block discarded – undo
26 26
 use Symfony\Component\Console\Question\ConfirmationQuestion;
27 27
 
28 28
 class TransferOwnership extends Command {
29
-	public function __construct(
30
-		private IUserManager $userManager,
31
-		private OwnershipTransferService $transferService,
32
-		private IConfig $config,
33
-		private IMountManager $mountManager,
34
-	) {
35
-		parent::__construct();
36
-	}
29
+    public function __construct(
30
+        private IUserManager $userManager,
31
+        private OwnershipTransferService $transferService,
32
+        private IConfig $config,
33
+        private IMountManager $mountManager,
34
+    ) {
35
+        parent::__construct();
36
+    }
37 37
 
38
-	protected function configure(): void {
39
-		$this
40
-			->setName('files:transfer-ownership')
41
-			->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.')
42
-			->addArgument(
43
-				'source-user',
44
-				InputArgument::REQUIRED,
45
-				'owner of files which shall be moved'
46
-			)
47
-			->addArgument(
48
-				'destination-user',
49
-				InputArgument::REQUIRED,
50
-				'user who will be the new owner of the files'
51
-			)
52
-			->addOption(
53
-				'path',
54
-				null,
55
-				InputOption::VALUE_REQUIRED,
56
-				'selectively provide the path to transfer. For example --path="folder_name"',
57
-				''
58
-			)->addOption(
59
-				'move',
60
-				null,
61
-				InputOption::VALUE_NONE,
62
-				'move data from source user to root directory of destination user, which must be empty'
63
-			)->addOption(
64
-				'transfer-incoming-shares',
65
-				null,
66
-				InputOption::VALUE_OPTIONAL,
67
-				'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore',
68
-				'2'
69
-			)->addOption(
70
-				'include-external-storage',
71
-				null,
72
-				InputOption::VALUE_NONE,
73
-				'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory',
74
-			)->addOption(
75
-				'force-include-external-storage',
76
-				null,
77
-				InputOption::VALUE_NONE,
78
-				'don\'t ask for confirmation for transferring external storages',
79
-			)
80
-			->addOption(
81
-				'use-user-id',
82
-				null,
83
-				InputOption::VALUE_NONE,
84
-				'use user ID instead of display name in the transferred folder name',
85
-			);
86
-	}
38
+    protected function configure(): void {
39
+        $this
40
+            ->setName('files:transfer-ownership')
41
+            ->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.')
42
+            ->addArgument(
43
+                'source-user',
44
+                InputArgument::REQUIRED,
45
+                'owner of files which shall be moved'
46
+            )
47
+            ->addArgument(
48
+                'destination-user',
49
+                InputArgument::REQUIRED,
50
+                'user who will be the new owner of the files'
51
+            )
52
+            ->addOption(
53
+                'path',
54
+                null,
55
+                InputOption::VALUE_REQUIRED,
56
+                'selectively provide the path to transfer. For example --path="folder_name"',
57
+                ''
58
+            )->addOption(
59
+                'move',
60
+                null,
61
+                InputOption::VALUE_NONE,
62
+                'move data from source user to root directory of destination user, which must be empty'
63
+            )->addOption(
64
+                'transfer-incoming-shares',
65
+                null,
66
+                InputOption::VALUE_OPTIONAL,
67
+                'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore',
68
+                '2'
69
+            )->addOption(
70
+                'include-external-storage',
71
+                null,
72
+                InputOption::VALUE_NONE,
73
+                'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory',
74
+            )->addOption(
75
+                'force-include-external-storage',
76
+                null,
77
+                InputOption::VALUE_NONE,
78
+                'don\'t ask for confirmation for transferring external storages',
79
+            )
80
+            ->addOption(
81
+                'use-user-id',
82
+                null,
83
+                InputOption::VALUE_NONE,
84
+                'use user ID instead of display name in the transferred folder name',
85
+            );
86
+    }
87 87
 
88
-	protected function execute(InputInterface $input, OutputInterface $output): int {
88
+    protected function execute(InputInterface $input, OutputInterface $output): int {
89 89
 
90
-		/**
91
-		 * Check if source and destination users are same. If they are same then just ignore the transfer.
92
-		 */
90
+        /**
91
+         * Check if source and destination users are same. If they are same then just ignore the transfer.
92
+         */
93 93
 
94
-		if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) {
95
-			$output->writeln("<error>Ownership can't be transferred when Source and Destination users are the same user. Please check your input.</error>");
96
-			return self::FAILURE;
97
-		}
94
+        if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) {
95
+            $output->writeln("<error>Ownership can't be transferred when Source and Destination users are the same user. Please check your input.</error>");
96
+            return self::FAILURE;
97
+        }
98 98
 
99
-		$sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
100
-		$destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
99
+        $sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
100
+        $destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
101 101
 
102
-		if (!$sourceUserObject instanceof IUser) {
103
-			$output->writeln('<error>Unknown source user ' . $input->getArgument('source-user') . '</error>');
104
-			return self::FAILURE;
105
-		}
102
+        if (!$sourceUserObject instanceof IUser) {
103
+            $output->writeln('<error>Unknown source user ' . $input->getArgument('source-user') . '</error>');
104
+            return self::FAILURE;
105
+        }
106 106
 
107
-		if (!$destinationUserObject instanceof IUser) {
108
-			$output->writeln('<error>Unknown destination user ' . $input->getArgument('destination-user') . '</error>');
109
-			return self::FAILURE;
110
-		}
107
+        if (!$destinationUserObject instanceof IUser) {
108
+            $output->writeln('<error>Unknown destination user ' . $input->getArgument('destination-user') . '</error>');
109
+            return self::FAILURE;
110
+        }
111 111
 
112
-		$path = ltrim($input->getOption('path'), '/');
113
-		$includeExternalStorage = $input->getOption('include-external-storage');
114
-		if ($includeExternalStorage) {
115
-			$mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/'));
116
-			/** @var IMountPoint[] $mounts */
117
-			$mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class);
118
-			if (count($mounts) > 0) {
119
-				$output->writeln(count($mounts) . ' external storages will be transferred:');
120
-				foreach ($mounts as $mount) {
121
-					$output->writeln('  - <info>' . $mount->getMountPoint() . '</info>');
122
-				}
123
-				$output->writeln('');
124
-				$output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>');
125
-				$output->writeln('');
126
-				if (!$input->getOption('force-include-external-storage')) {
127
-					/** @var QuestionHelper $helper */
128
-					$helper = $this->getHelper('question');
129
-					$question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false);
130
-					if (!$helper->ask($input, $output, $question)) {
131
-						return self::FAILURE;
132
-					}
133
-				}
134
-			}
135
-		}
112
+        $path = ltrim($input->getOption('path'), '/');
113
+        $includeExternalStorage = $input->getOption('include-external-storage');
114
+        if ($includeExternalStorage) {
115
+            $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/'));
116
+            /** @var IMountPoint[] $mounts */
117
+            $mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class);
118
+            if (count($mounts) > 0) {
119
+                $output->writeln(count($mounts) . ' external storages will be transferred:');
120
+                foreach ($mounts as $mount) {
121
+                    $output->writeln('  - <info>' . $mount->getMountPoint() . '</info>');
122
+                }
123
+                $output->writeln('');
124
+                $output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>');
125
+                $output->writeln('');
126
+                if (!$input->getOption('force-include-external-storage')) {
127
+                    /** @var QuestionHelper $helper */
128
+                    $helper = $this->getHelper('question');
129
+                    $question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false);
130
+                    if (!$helper->ask($input, $output, $question)) {
131
+                        return self::FAILURE;
132
+                    }
133
+                }
134
+            }
135
+        }
136 136
 
137
-		try {
138
-			$this->transferService->transfer(
139
-				$sourceUserObject,
140
-				$destinationUserObject,
141
-				$path,
142
-				$output,
143
-				$input->getOption('move') === true,
144
-				false,
145
-				$includeExternalStorage,
146
-				$input->getOption('use-user-id') === true,
147
-			);
148
-		} catch (TransferOwnershipException $e) {
149
-			$output->writeln('<error>' . $e->getMessage() . '</error>');
150
-			return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
151
-		}
137
+        try {
138
+            $this->transferService->transfer(
139
+                $sourceUserObject,
140
+                $destinationUserObject,
141
+                $path,
142
+                $output,
143
+                $input->getOption('move') === true,
144
+                false,
145
+                $includeExternalStorage,
146
+                $input->getOption('use-user-id') === true,
147
+            );
148
+        } catch (TransferOwnershipException $e) {
149
+            $output->writeln('<error>' . $e->getMessage() . '</error>');
150
+            return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
151
+        }
152 152
 
153
-		return self::SUCCESS;
154
-	}
153
+        return self::SUCCESS;
154
+    }
155 155
 }
Please login to merge, or discard this patch.
Spacing   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -100,25 +100,25 @@  discard block
 block discarded – undo
100 100
 		$destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
101 101
 
102 102
 		if (!$sourceUserObject instanceof IUser) {
103
-			$output->writeln('<error>Unknown source user ' . $input->getArgument('source-user') . '</error>');
103
+			$output->writeln('<error>Unknown source user '.$input->getArgument('source-user').'</error>');
104 104
 			return self::FAILURE;
105 105
 		}
106 106
 
107 107
 		if (!$destinationUserObject instanceof IUser) {
108
-			$output->writeln('<error>Unknown destination user ' . $input->getArgument('destination-user') . '</error>');
108
+			$output->writeln('<error>Unknown destination user '.$input->getArgument('destination-user').'</error>');
109 109
 			return self::FAILURE;
110 110
 		}
111 111
 
112 112
 		$path = ltrim($input->getOption('path'), '/');
113 113
 		$includeExternalStorage = $input->getOption('include-external-storage');
114 114
 		if ($includeExternalStorage) {
115
-			$mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/'));
115
+			$mounts = $this->mountManager->findIn('/'.rtrim($sourceUserObject->getUID().'/files/'.$path, '/'));
116 116
 			/** @var IMountPoint[] $mounts */
117 117
 			$mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class);
118 118
 			if (count($mounts) > 0) {
119
-				$output->writeln(count($mounts) . ' external storages will be transferred:');
119
+				$output->writeln(count($mounts).' external storages will be transferred:');
120 120
 				foreach ($mounts as $mount) {
121
-					$output->writeln('  - <info>' . $mount->getMountPoint() . '</info>');
121
+					$output->writeln('  - <info>'.$mount->getMountPoint().'</info>');
122 122
 				}
123 123
 				$output->writeln('');
124 124
 				$output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>');
@@ -146,7 +146,7 @@  discard block
 block discarded – undo
146 146
 				$input->getOption('use-user-id') === true,
147 147
 			);
148 148
 		} catch (TransferOwnershipException $e) {
149
-			$output->writeln('<error>' . $e->getMessage() . '</error>');
149
+			$output->writeln('<error>'.$e->getMessage().'</error>');
150 150
 			return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
151 151
 		}
152 152
 
Please login to merge, or discard this patch.
apps/files/lib/Service/OwnershipTransferService.php 2 patches
Indentation   +586 added lines, -586 removed lines patch added patch discarded remove patch
@@ -44,590 +44,590 @@
 block discarded – undo
44 44
 
45 45
 class OwnershipTransferService {
46 46
 
47
-	public function __construct(
48
-		private IEncryptionManager $encryptionManager,
49
-		private IShareManager $shareManager,
50
-		private IMountManager $mountManager,
51
-		private IUserMountCache $userMountCache,
52
-		private IUserManager $userManager,
53
-		private IFactory $l10nFactory,
54
-		private IRootFolder $rootFolder,
55
-	) {
56
-	}
57
-
58
-	/**
59
-	 * @param IUser $sourceUser
60
-	 * @param IUser $destinationUser
61
-	 * @param string $path
62
-	 *
63
-	 * @param OutputInterface|null $output
64
-	 * @param bool $move
65
-	 * @throws TransferOwnershipException
66
-	 * @throws NoUserException
67
-	 */
68
-	public function transfer(
69
-		IUser $sourceUser,
70
-		IUser $destinationUser,
71
-		string $path,
72
-		?OutputInterface $output = null,
73
-		bool $move = false,
74
-		bool $firstLogin = false,
75
-		bool $includeExternalStorage = false,
76
-		bool $useUserId = false,
77
-	): void {
78
-		$output = $output ?? new NullOutput();
79
-		$sourceUid = $sourceUser->getUID();
80
-		$destinationUid = $destinationUser->getUID();
81
-		$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
82
-
83
-		// If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
84
-		if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
85
-			|| !$this->encryptionManager->isReadyForUser($destinationUid)) {
86
-			throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
87
-		}
88
-
89
-		// setup filesystem
90
-		// Requesting the user folder will set it up if the user hasn't logged in before
91
-		// We need a setupFS for the full filesystem setup before as otherwise we will just return
92
-		// a lazy root folder which does not create the destination users folder
93
-		\OC_Util::setupFS($sourceUser->getUID());
94
-		\OC_Util::setupFS($destinationUser->getUID());
95
-		$this->rootFolder->getUserFolder($sourceUser->getUID());
96
-		$this->rootFolder->getUserFolder($destinationUser->getUID());
97
-		Filesystem::initMountPoints($sourceUid);
98
-		Filesystem::initMountPoints($destinationUid);
99
-
100
-		$view = new View();
101
-
102
-		if ($move) {
103
-			$finalTarget = "$destinationUid/files/";
104
-		} else {
105
-			$l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
106
-			$date = date('Y-m-d H-i-s');
107
-
108
-			if ($useUserId) {
109
-				$cleanUserName = $sourceUid;
110
-			} else {
111
-				$cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName());
112
-				if ($cleanUserName === '') {
113
-					$cleanUserName = $sourceUid;
114
-				}
115
-			}
116
-
117
-			$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
118
-			try {
119
-				$view->verifyPath(dirname($finalTarget), basename($finalTarget));
120
-			} catch (InvalidPathException $e) {
121
-				$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
122
-			}
123
-		}
124
-
125
-		if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
126
-			throw new TransferOwnershipException("Unknown path provided: $path", 1);
127
-		}
128
-
129
-		if ($move && !$view->is_dir($finalTarget)) {
130
-			// Initialize storage
131
-			\OC_Util::setupFS($destinationUser->getUID());
132
-		}
133
-
134
-		if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
135
-			throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
136
-		}
137
-
138
-
139
-		// analyse source folder
140
-		$this->analyse(
141
-			$sourceUid,
142
-			$destinationUid,
143
-			$sourcePath,
144
-			$view,
145
-			$output
146
-		);
147
-
148
-		// collect all the shares
149
-		$shares = $this->collectUsersShares(
150
-			$sourceUid,
151
-			$output,
152
-			$view,
153
-			$sourcePath
154
-		);
155
-
156
-		$sourceSize = $view->getFileInfo($sourcePath)->getSize();
157
-
158
-		// transfer the files
159
-		$this->transferFiles(
160
-			$sourceUid,
161
-			$sourcePath,
162
-			$finalTarget,
163
-			$view,
164
-			$output,
165
-			$includeExternalStorage,
166
-		);
167
-		$sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
168
-
169
-		// transfer the incoming shares
170
-		$sourceShares = $this->collectIncomingShares(
171
-			$sourceUid,
172
-			$output,
173
-			$sourcePath,
174
-		);
175
-		$destinationShares = $this->collectIncomingShares(
176
-			$destinationUid,
177
-			$output,
178
-			null,
179
-		);
180
-		$this->transferIncomingShares(
181
-			$sourceUid,
182
-			$destinationUid,
183
-			$sourceShares,
184
-			$destinationShares,
185
-			$output,
186
-			$path,
187
-			$finalTarget,
188
-			$move
189
-		);
190
-
191
-		$destinationPath = $finalTarget . '/' . $path;
192
-		// restore the shares
193
-		$this->restoreShares(
194
-			$sourceUid,
195
-			$destinationUid,
196
-			$destinationPath,
197
-			$shares,
198
-			$output
199
-		);
200
-		if ($sizeDifference !== 0) {
201
-			$output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
202
-		}
203
-	}
204
-
205
-	private function sanitizeFolderName(string $name): string {
206
-		// Remove some characters which are prone to cause errors
207
-		$name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
208
-		// Replace multiple dashes with one dash
209
-		return preg_replace('/-{2,}/s', '-', $name);
210
-	}
211
-
212
-	private function walkFiles(View $view, $path, Closure $callBack) {
213
-		foreach ($view->getDirectoryContent($path) as $fileInfo) {
214
-			if (!$callBack($fileInfo)) {
215
-				return;
216
-			}
217
-			if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
218
-				$this->walkFiles($view, $fileInfo->getPath(), $callBack);
219
-			}
220
-		}
221
-	}
222
-
223
-	/**
224
-	 * @param OutputInterface $output
225
-	 *
226
-	 * @throws TransferOwnershipException
227
-	 */
228
-	protected function analyse(
229
-		string $sourceUid,
230
-		string $destinationUid,
231
-		string $sourcePath,
232
-		View $view,
233
-		OutputInterface $output,
234
-		bool $includeExternalStorage = false,
235
-	): void {
236
-		$output->writeln('Validating quota');
237
-		$sourceFileInfo = $view->getFileInfo($sourcePath, false);
238
-		if ($sourceFileInfo === false) {
239
-			throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
240
-		}
241
-		$size = $sourceFileInfo->getSize(false);
242
-		$freeSpace = $view->free_space($destinationUid . '/files/');
243
-		if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
244
-			throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
245
-		}
246
-
247
-		$output->writeln("Analysing files of $sourceUid ...");
248
-		$progress = new ProgressBar($output);
249
-		$progress->start();
250
-
251
-		if ($this->encryptionManager->isEnabled()) {
252
-			$masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
253
-		} else {
254
-			$masterKeyEnabled = false;
255
-		}
256
-		$encryptedFiles = [];
257
-		if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
258
-			if ($sourceFileInfo->isEncrypted()) {
259
-				/* Encrypted folder means e2ee encrypted */
260
-				$encryptedFiles[] = $sourceFileInfo;
261
-			} else {
262
-				$this->walkFiles($view, $sourcePath,
263
-					function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
264
-						if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
265
-							$mount = $fileInfo->getMountPoint();
266
-							// only analyze into folders from main storage,
267
-							if (
268
-								$mount->getMountProvider() instanceof IHomeMountProvider
269
-								|| ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
270
-							) {
271
-								if ($fileInfo->isEncrypted()) {
272
-									/* Encrypted folder means e2ee encrypted, we cannot transfer it */
273
-									$encryptedFiles[] = $fileInfo;
274
-								}
275
-								return true;
276
-							} else {
277
-								return false;
278
-							}
279
-						}
280
-						$progress->advance();
281
-						if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
282
-							/* Encrypted file means SSE, we can only transfer it if master key is enabled */
283
-							$encryptedFiles[] = $fileInfo;
284
-						}
285
-						return true;
286
-					});
287
-			}
288
-		} elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
289
-			/* Encrypted file means SSE, we can only transfer it if master key is enabled */
290
-			$encryptedFiles[] = $sourceFileInfo;
291
-		}
292
-		$progress->finish();
293
-		$output->writeln('');
294
-
295
-		// no file is allowed to be encrypted
296
-		if (!empty($encryptedFiles)) {
297
-			$output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
298
-			foreach ($encryptedFiles as $encryptedFile) {
299
-				/** @var FileInfo $encryptedFile */
300
-				$output->writeln('  ' . $encryptedFile->getPath());
301
-			}
302
-			throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
303
-		}
304
-	}
305
-
306
-	/**
307
-	 * @return array<array{share: IShare, suffix: string}>
308
-	 */
309
-	private function collectUsersShares(
310
-		string $sourceUid,
311
-		OutputInterface $output,
312
-		View $view,
313
-		string $path,
314
-	): array {
315
-		$output->writeln("Collecting all share information for files and folders of $sourceUid ...");
316
-
317
-		$shares = [];
318
-		$progress = new ProgressBar($output);
319
-
320
-		$normalizedPath = Filesystem::normalizePath($path);
321
-
322
-		$supportedShareTypes = [
323
-			IShare::TYPE_GROUP,
324
-			IShare::TYPE_USER,
325
-			IShare::TYPE_LINK,
326
-			IShare::TYPE_REMOTE,
327
-			IShare::TYPE_ROOM,
328
-			IShare::TYPE_EMAIL,
329
-			IShare::TYPE_CIRCLE,
330
-			IShare::TYPE_DECK,
331
-			IShare::TYPE_SCIENCEMESH,
332
-		];
333
-
334
-		foreach ($supportedShareTypes as $shareType) {
335
-			$offset = 0;
336
-			while (true) {
337
-				$sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
338
-				$progress->advance(count($sharePage));
339
-				if (empty($sharePage)) {
340
-					break;
341
-				}
342
-				if ($path !== "$sourceUid/files") {
343
-					$sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
344
-						try {
345
-							$sourceNode = $share->getNode();
346
-							$relativePath = $view->getRelativePath($sourceNode->getPath());
347
-
348
-							return str_starts_with($relativePath . '/', $normalizedPath . '/');
349
-						} catch (Exception $e) {
350
-							return false;
351
-						}
352
-					});
353
-				}
354
-				$shares = array_merge($shares, $sharePage);
355
-				$offset += 50;
356
-			}
357
-		}
358
-
359
-		$progress->finish();
360
-		$output->writeln('');
361
-
362
-		return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
363
-			try {
364
-				$nodePath = $view->getRelativePath($share->getNode()->getPath());
365
-			} catch (NotFoundException $e) {
366
-				$output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
367
-				return null;
368
-			}
369
-
370
-			return [
371
-				'share' => $share,
372
-				'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
373
-			];
374
-		}, $shares)));
375
-	}
376
-
377
-	private function collectIncomingShares(
378
-		string $sourceUid,
379
-		OutputInterface $output,
380
-		?string $path,
381
-	): array {
382
-		$output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
383
-
384
-		$shares = [];
385
-		$progress = new ProgressBar($output);
386
-		$normalizedPath = Filesystem::normalizePath($path);
387
-
388
-		$offset = 0;
389
-		while (true) {
390
-			$sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
391
-			$progress->advance(count($sharePage));
392
-			if (empty($sharePage)) {
393
-				break;
394
-			}
395
-
396
-			if ($path !== null && $path !== "$sourceUid/files") {
397
-				$sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
398
-					try {
399
-						return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
400
-					} catch (Exception) {
401
-						return false;
402
-					}
403
-				});
404
-			}
405
-
406
-			foreach ($sharePage as $share) {
407
-				$shares[$share->getNodeId()] = $share;
408
-			}
409
-
410
-			$offset += 50;
411
-		}
412
-
413
-
414
-		$progress->finish();
415
-		$output->writeln('');
416
-		return $shares;
417
-	}
418
-
419
-	/**
420
-	 * @throws TransferOwnershipException
421
-	 */
422
-	protected function transferFiles(
423
-		string $sourceUid,
424
-		string $sourcePath,
425
-		string $finalTarget,
426
-		View $view,
427
-		OutputInterface $output,
428
-		bool $includeExternalStorage,
429
-	): void {
430
-		$output->writeln("Transferring files to $finalTarget ...");
431
-
432
-		// This change will help user to transfer the folder specified using --path option.
433
-		// Else only the content inside folder is transferred which is not correct.
434
-		if ($sourcePath !== "$sourceUid/files") {
435
-			$view->mkdir($finalTarget);
436
-			$finalTarget = $finalTarget . '/' . basename($sourcePath);
437
-		}
438
-		$sourceInfo = $view->getFileInfo($sourcePath);
439
-
440
-		/// handle the external storages mounted at the root, or the admin specifying an external storage with --path
441
-		if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
442
-			$this->moveMountContents($view, $sourcePath, $finalTarget);
443
-		} else {
444
-			if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
445
-				throw new TransferOwnershipException('Could not transfer files.', 1);
446
-			}
447
-		}
448
-
449
-		if ($includeExternalStorage) {
450
-			$nestedMounts = $this->mountManager->findIn($sourcePath);
451
-			foreach ($nestedMounts as $mount) {
452
-				if ($mount->getMountProvider() === ConfigAdapter::class) {
453
-					$relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
454
-					$this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
455
-				}
456
-			}
457
-		}
458
-
459
-		if (!is_dir("$sourceUid/files")) {
460
-			// because the files folder is moved away we need to recreate it
461
-			$view->mkdir("$sourceUid/files");
462
-		}
463
-	}
464
-
465
-	private function moveMountContents(View $rootView, string $source, string $target) {
466
-		if ($rootView->copy($source, $target)) {
467
-			// just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
468
-			// we need to empty the contents instead
469
-			$content = $rootView->getDirectoryContent($source);
470
-			foreach ($content as $item) {
471
-				if ($item->getType() === FileInfo::TYPE_FOLDER) {
472
-					$rootView->rmdir($item->getPath());
473
-				} else {
474
-					$rootView->unlink($item->getPath());
475
-				}
476
-			}
477
-		} else {
478
-			throw new TransferOwnershipException("Could not transfer $source to $target");
479
-		}
480
-	}
481
-
482
-	/**
483
-	 * @param string $targetLocation New location of the transfered node
484
-	 * @param array<array{share: IShare, suffix: string}> $shares previously collected share information
485
-	 */
486
-	private function restoreShares(
487
-		string $sourceUid,
488
-		string $destinationUid,
489
-		string $targetLocation,
490
-		array $shares,
491
-		OutputInterface $output,
492
-	):void {
493
-		$output->writeln('Restoring shares ...');
494
-		$progress = new ProgressBar($output, count($shares));
495
-
496
-		foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
497
-			try {
498
-				$output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
499
-				if ($share->getShareType() === IShare::TYPE_USER
500
-					&& $share->getSharedWith() === $destinationUid) {
501
-					// Unmount the shares before deleting, so we don't try to get the storage later on.
502
-					$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
503
-					if ($shareMountPoint) {
504
-						$this->mountManager->removeMount($shareMountPoint->getMountPoint());
505
-					}
506
-					$this->shareManager->deleteShare($share);
507
-				} else {
508
-					if ($share->getShareOwner() === $sourceUid) {
509
-						$share->setShareOwner($destinationUid);
510
-					}
511
-					if ($share->getSharedBy() === $sourceUid) {
512
-						$share->setSharedBy($destinationUid);
513
-					}
514
-
515
-					if ($share->getShareType() === IShare::TYPE_USER
516
-						&& !$this->userManager->userExists($share->getSharedWith())) {
517
-						// stray share with deleted user
518
-						$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
519
-						$this->shareManager->deleteShare($share);
520
-						continue;
521
-					} else {
522
-						// trigger refetching of the node so that the new owner and mountpoint are taken into account
523
-						// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
524
-						$this->userMountCache->clear();
525
-
526
-						try {
527
-							// Try to get the "old" id.
528
-							// Normally the ID is preserved,
529
-							// but for transferes between different storages the ID might change
530
-							$newNodeId = $share->getNode()->getId();
531
-						} catch (NotFoundException) {
532
-							// ID has changed due to transfer between different storages
533
-							// Try to get the new ID from the target path and suffix of the share
534
-							$node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
535
-							$newNodeId = $node->getId();
536
-							$output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
537
-						}
538
-						$share->setNodeId($newNodeId);
539
-
540
-						$this->shareManager->updateShare($share, onlyValid: false);
541
-					}
542
-				}
543
-			} catch (NotFoundException $e) {
544
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
545
-			} catch (\Throwable $e) {
546
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
547
-			}
548
-			$progress->advance();
549
-		}
550
-		$progress->finish();
551
-		$output->writeln('');
552
-	}
553
-
554
-	private function transferIncomingShares(string $sourceUid,
555
-		string $destinationUid,
556
-		array $sourceShares,
557
-		array $destinationShares,
558
-		OutputInterface $output,
559
-		string $path,
560
-		string $finalTarget,
561
-		bool $move): void {
562
-		$output->writeln('Restoring incoming shares ...');
563
-		$progress = new ProgressBar($output, count($sourceShares));
564
-		$prefix = "$destinationUid/files";
565
-		$finalShareTarget = '';
566
-		if (str_starts_with($finalTarget, $prefix)) {
567
-			$finalShareTarget = substr($finalTarget, strlen($prefix));
568
-		}
569
-		foreach ($sourceShares as $share) {
570
-			try {
571
-				// Only restore if share is in given path.
572
-				$pathToCheck = '/';
573
-				if (trim($path, '/') !== '') {
574
-					$pathToCheck = '/' . trim($path) . '/';
575
-				}
576
-				if (!str_starts_with($share->getTarget(), $pathToCheck)) {
577
-					continue;
578
-				}
579
-				$shareTarget = $share->getTarget();
580
-				$shareTarget = $finalShareTarget . $shareTarget;
581
-				if ($share->getShareType() === IShare::TYPE_USER
582
-					&& $share->getSharedBy() === $destinationUid) {
583
-					$this->shareManager->deleteShare($share);
584
-				} elseif (isset($destinationShares[$share->getNodeId()])) {
585
-					$destinationShare = $destinationShares[$share->getNodeId()];
586
-					// Keep the share which has the most permissions and discard the other one.
587
-					if ($destinationShare->getPermissions() < $share->getPermissions()) {
588
-						$this->shareManager->deleteShare($destinationShare);
589
-						$share->setSharedWith($destinationUid);
590
-						// trigger refetching of the node so that the new owner and mountpoint are taken into account
591
-						// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
592
-						$this->userMountCache->clear();
593
-						$share->setNodeId($share->getNode()->getId());
594
-						$this->shareManager->updateShare($share);
595
-						// The share is already transferred.
596
-						$progress->advance();
597
-						if ($move) {
598
-							continue;
599
-						}
600
-						$share->setTarget($shareTarget);
601
-						$this->shareManager->moveShare($share, $destinationUid);
602
-						continue;
603
-					}
604
-					$this->shareManager->deleteShare($share);
605
-				} elseif ($share->getShareOwner() === $destinationUid) {
606
-					$this->shareManager->deleteShare($share);
607
-				} else {
608
-					$share->setSharedWith($destinationUid);
609
-					$share->setNodeId($share->getNode()->getId());
610
-					$this->shareManager->updateShare($share);
611
-					// trigger refetching of the node so that the new owner and mountpoint are taken into account
612
-					// otherwise the checks on the share update will fail due to the original node not being available in the new user scope
613
-					$this->userMountCache->clear();
614
-					// The share is already transferred.
615
-					$progress->advance();
616
-					if ($move) {
617
-						continue;
618
-					}
619
-					$share->setTarget($shareTarget);
620
-					$this->shareManager->moveShare($share, $destinationUid);
621
-					continue;
622
-				}
623
-			} catch (NotFoundException $e) {
624
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
625
-			} catch (\Throwable $e) {
626
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
627
-			}
628
-			$progress->advance();
629
-		}
630
-		$progress->finish();
631
-		$output->writeln('');
632
-	}
47
+    public function __construct(
48
+        private IEncryptionManager $encryptionManager,
49
+        private IShareManager $shareManager,
50
+        private IMountManager $mountManager,
51
+        private IUserMountCache $userMountCache,
52
+        private IUserManager $userManager,
53
+        private IFactory $l10nFactory,
54
+        private IRootFolder $rootFolder,
55
+    ) {
56
+    }
57
+
58
+    /**
59
+     * @param IUser $sourceUser
60
+     * @param IUser $destinationUser
61
+     * @param string $path
62
+     *
63
+     * @param OutputInterface|null $output
64
+     * @param bool $move
65
+     * @throws TransferOwnershipException
66
+     * @throws NoUserException
67
+     */
68
+    public function transfer(
69
+        IUser $sourceUser,
70
+        IUser $destinationUser,
71
+        string $path,
72
+        ?OutputInterface $output = null,
73
+        bool $move = false,
74
+        bool $firstLogin = false,
75
+        bool $includeExternalStorage = false,
76
+        bool $useUserId = false,
77
+    ): void {
78
+        $output = $output ?? new NullOutput();
79
+        $sourceUid = $sourceUser->getUID();
80
+        $destinationUid = $destinationUser->getUID();
81
+        $sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
82
+
83
+        // If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
84
+        if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
85
+            || !$this->encryptionManager->isReadyForUser($destinationUid)) {
86
+            throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2);
87
+        }
88
+
89
+        // setup filesystem
90
+        // Requesting the user folder will set it up if the user hasn't logged in before
91
+        // We need a setupFS for the full filesystem setup before as otherwise we will just return
92
+        // a lazy root folder which does not create the destination users folder
93
+        \OC_Util::setupFS($sourceUser->getUID());
94
+        \OC_Util::setupFS($destinationUser->getUID());
95
+        $this->rootFolder->getUserFolder($sourceUser->getUID());
96
+        $this->rootFolder->getUserFolder($destinationUser->getUID());
97
+        Filesystem::initMountPoints($sourceUid);
98
+        Filesystem::initMountPoints($destinationUid);
99
+
100
+        $view = new View();
101
+
102
+        if ($move) {
103
+            $finalTarget = "$destinationUid/files/";
104
+        } else {
105
+            $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
106
+            $date = date('Y-m-d H-i-s');
107
+
108
+            if ($useUserId) {
109
+                $cleanUserName = $sourceUid;
110
+            } else {
111
+                $cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName());
112
+                if ($cleanUserName === '') {
113
+                    $cleanUserName = $sourceUid;
114
+                }
115
+            }
116
+
117
+            $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
118
+            try {
119
+                $view->verifyPath(dirname($finalTarget), basename($finalTarget));
120
+            } catch (InvalidPathException $e) {
121
+                $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
122
+            }
123
+        }
124
+
125
+        if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
126
+            throw new TransferOwnershipException("Unknown path provided: $path", 1);
127
+        }
128
+
129
+        if ($move && !$view->is_dir($finalTarget)) {
130
+            // Initialize storage
131
+            \OC_Util::setupFS($destinationUser->getUID());
132
+        }
133
+
134
+        if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
135
+            throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
136
+        }
137
+
138
+
139
+        // analyse source folder
140
+        $this->analyse(
141
+            $sourceUid,
142
+            $destinationUid,
143
+            $sourcePath,
144
+            $view,
145
+            $output
146
+        );
147
+
148
+        // collect all the shares
149
+        $shares = $this->collectUsersShares(
150
+            $sourceUid,
151
+            $output,
152
+            $view,
153
+            $sourcePath
154
+        );
155
+
156
+        $sourceSize = $view->getFileInfo($sourcePath)->getSize();
157
+
158
+        // transfer the files
159
+        $this->transferFiles(
160
+            $sourceUid,
161
+            $sourcePath,
162
+            $finalTarget,
163
+            $view,
164
+            $output,
165
+            $includeExternalStorage,
166
+        );
167
+        $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize();
168
+
169
+        // transfer the incoming shares
170
+        $sourceShares = $this->collectIncomingShares(
171
+            $sourceUid,
172
+            $output,
173
+            $sourcePath,
174
+        );
175
+        $destinationShares = $this->collectIncomingShares(
176
+            $destinationUid,
177
+            $output,
178
+            null,
179
+        );
180
+        $this->transferIncomingShares(
181
+            $sourceUid,
182
+            $destinationUid,
183
+            $sourceShares,
184
+            $destinationShares,
185
+            $output,
186
+            $path,
187
+            $finalTarget,
188
+            $move
189
+        );
190
+
191
+        $destinationPath = $finalTarget . '/' . $path;
192
+        // restore the shares
193
+        $this->restoreShares(
194
+            $sourceUid,
195
+            $destinationUid,
196
+            $destinationPath,
197
+            $shares,
198
+            $output
199
+        );
200
+        if ($sizeDifference !== 0) {
201
+            $output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation.");
202
+        }
203
+    }
204
+
205
+    private function sanitizeFolderName(string $name): string {
206
+        // Remove some characters which are prone to cause errors
207
+        $name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name);
208
+        // Replace multiple dashes with one dash
209
+        return preg_replace('/-{2,}/s', '-', $name);
210
+    }
211
+
212
+    private function walkFiles(View $view, $path, Closure $callBack) {
213
+        foreach ($view->getDirectoryContent($path) as $fileInfo) {
214
+            if (!$callBack($fileInfo)) {
215
+                return;
216
+            }
217
+            if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
218
+                $this->walkFiles($view, $fileInfo->getPath(), $callBack);
219
+            }
220
+        }
221
+    }
222
+
223
+    /**
224
+     * @param OutputInterface $output
225
+     *
226
+     * @throws TransferOwnershipException
227
+     */
228
+    protected function analyse(
229
+        string $sourceUid,
230
+        string $destinationUid,
231
+        string $sourcePath,
232
+        View $view,
233
+        OutputInterface $output,
234
+        bool $includeExternalStorage = false,
235
+    ): void {
236
+        $output->writeln('Validating quota');
237
+        $sourceFileInfo = $view->getFileInfo($sourcePath, false);
238
+        if ($sourceFileInfo === false) {
239
+            throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
240
+        }
241
+        $size = $sourceFileInfo->getSize(false);
242
+        $freeSpace = $view->free_space($destinationUid . '/files/');
243
+        if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
244
+            throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
245
+        }
246
+
247
+        $output->writeln("Analysing files of $sourceUid ...");
248
+        $progress = new ProgressBar($output);
249
+        $progress->start();
250
+
251
+        if ($this->encryptionManager->isEnabled()) {
252
+            $masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled();
253
+        } else {
254
+            $masterKeyEnabled = false;
255
+        }
256
+        $encryptedFiles = [];
257
+        if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) {
258
+            if ($sourceFileInfo->isEncrypted()) {
259
+                /* Encrypted folder means e2ee encrypted */
260
+                $encryptedFiles[] = $sourceFileInfo;
261
+            } else {
262
+                $this->walkFiles($view, $sourcePath,
263
+                    function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
264
+                        if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
265
+                            $mount = $fileInfo->getMountPoint();
266
+                            // only analyze into folders from main storage,
267
+                            if (
268
+                                $mount->getMountProvider() instanceof IHomeMountProvider
269
+                                || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter)
270
+                            ) {
271
+                                if ($fileInfo->isEncrypted()) {
272
+                                    /* Encrypted folder means e2ee encrypted, we cannot transfer it */
273
+                                    $encryptedFiles[] = $fileInfo;
274
+                                }
275
+                                return true;
276
+                            } else {
277
+                                return false;
278
+                            }
279
+                        }
280
+                        $progress->advance();
281
+                        if ($fileInfo->isEncrypted() && !$masterKeyEnabled) {
282
+                            /* Encrypted file means SSE, we can only transfer it if master key is enabled */
283
+                            $encryptedFiles[] = $fileInfo;
284
+                        }
285
+                        return true;
286
+                    });
287
+            }
288
+        } elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) {
289
+            /* Encrypted file means SSE, we can only transfer it if master key is enabled */
290
+            $encryptedFiles[] = $sourceFileInfo;
291
+        }
292
+        $progress->finish();
293
+        $output->writeln('');
294
+
295
+        // no file is allowed to be encrypted
296
+        if (!empty($encryptedFiles)) {
297
+            $output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
298
+            foreach ($encryptedFiles as $encryptedFile) {
299
+                /** @var FileInfo $encryptedFile */
300
+                $output->writeln('  ' . $encryptedFile->getPath());
301
+            }
302
+            throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
303
+        }
304
+    }
305
+
306
+    /**
307
+     * @return array<array{share: IShare, suffix: string}>
308
+     */
309
+    private function collectUsersShares(
310
+        string $sourceUid,
311
+        OutputInterface $output,
312
+        View $view,
313
+        string $path,
314
+    ): array {
315
+        $output->writeln("Collecting all share information for files and folders of $sourceUid ...");
316
+
317
+        $shares = [];
318
+        $progress = new ProgressBar($output);
319
+
320
+        $normalizedPath = Filesystem::normalizePath($path);
321
+
322
+        $supportedShareTypes = [
323
+            IShare::TYPE_GROUP,
324
+            IShare::TYPE_USER,
325
+            IShare::TYPE_LINK,
326
+            IShare::TYPE_REMOTE,
327
+            IShare::TYPE_ROOM,
328
+            IShare::TYPE_EMAIL,
329
+            IShare::TYPE_CIRCLE,
330
+            IShare::TYPE_DECK,
331
+            IShare::TYPE_SCIENCEMESH,
332
+        ];
333
+
334
+        foreach ($supportedShareTypes as $shareType) {
335
+            $offset = 0;
336
+            while (true) {
337
+                $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false);
338
+                $progress->advance(count($sharePage));
339
+                if (empty($sharePage)) {
340
+                    break;
341
+                }
342
+                if ($path !== "$sourceUid/files") {
343
+                    $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
344
+                        try {
345
+                            $sourceNode = $share->getNode();
346
+                            $relativePath = $view->getRelativePath($sourceNode->getPath());
347
+
348
+                            return str_starts_with($relativePath . '/', $normalizedPath . '/');
349
+                        } catch (Exception $e) {
350
+                            return false;
351
+                        }
352
+                    });
353
+                }
354
+                $shares = array_merge($shares, $sharePage);
355
+                $offset += 50;
356
+            }
357
+        }
358
+
359
+        $progress->finish();
360
+        $output->writeln('');
361
+
362
+        return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
363
+            try {
364
+                $nodePath = $view->getRelativePath($share->getNode()->getPath());
365
+            } catch (NotFoundException $e) {
366
+                $output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>");
367
+                return null;
368
+            }
369
+
370
+            return [
371
+                'share' => $share,
372
+                'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)),
373
+            ];
374
+        }, $shares)));
375
+    }
376
+
377
+    private function collectIncomingShares(
378
+        string $sourceUid,
379
+        OutputInterface $output,
380
+        ?string $path,
381
+    ): array {
382
+        $output->writeln("Collecting all incoming share information for files and folders of $sourceUid ...");
383
+
384
+        $shares = [];
385
+        $progress = new ProgressBar($output);
386
+        $normalizedPath = Filesystem::normalizePath($path);
387
+
388
+        $offset = 0;
389
+        while (true) {
390
+            $sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
391
+            $progress->advance(count($sharePage));
392
+            if (empty($sharePage)) {
393
+                break;
394
+            }
395
+
396
+            if ($path !== null && $path !== "$sourceUid/files") {
397
+                $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
398
+                    try {
399
+                        return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
400
+                    } catch (Exception) {
401
+                        return false;
402
+                    }
403
+                });
404
+            }
405
+
406
+            foreach ($sharePage as $share) {
407
+                $shares[$share->getNodeId()] = $share;
408
+            }
409
+
410
+            $offset += 50;
411
+        }
412
+
413
+
414
+        $progress->finish();
415
+        $output->writeln('');
416
+        return $shares;
417
+    }
418
+
419
+    /**
420
+     * @throws TransferOwnershipException
421
+     */
422
+    protected function transferFiles(
423
+        string $sourceUid,
424
+        string $sourcePath,
425
+        string $finalTarget,
426
+        View $view,
427
+        OutputInterface $output,
428
+        bool $includeExternalStorage,
429
+    ): void {
430
+        $output->writeln("Transferring files to $finalTarget ...");
431
+
432
+        // This change will help user to transfer the folder specified using --path option.
433
+        // Else only the content inside folder is transferred which is not correct.
434
+        if ($sourcePath !== "$sourceUid/files") {
435
+            $view->mkdir($finalTarget);
436
+            $finalTarget = $finalTarget . '/' . basename($sourcePath);
437
+        }
438
+        $sourceInfo = $view->getFileInfo($sourcePath);
439
+
440
+        /// handle the external storages mounted at the root, or the admin specifying an external storage with --path
441
+        if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) {
442
+            $this->moveMountContents($view, $sourcePath, $finalTarget);
443
+        } else {
444
+            if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) {
445
+                throw new TransferOwnershipException('Could not transfer files.', 1);
446
+            }
447
+        }
448
+
449
+        if ($includeExternalStorage) {
450
+            $nestedMounts = $this->mountManager->findIn($sourcePath);
451
+            foreach ($nestedMounts as $mount) {
452
+                if ($mount->getMountProvider() === ConfigAdapter::class) {
453
+                    $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
454
+                    $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
455
+                }
456
+            }
457
+        }
458
+
459
+        if (!is_dir("$sourceUid/files")) {
460
+            // because the files folder is moved away we need to recreate it
461
+            $view->mkdir("$sourceUid/files");
462
+        }
463
+    }
464
+
465
+    private function moveMountContents(View $rootView, string $source, string $target) {
466
+        if ($rootView->copy($source, $target)) {
467
+            // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage
468
+            // we need to empty the contents instead
469
+            $content = $rootView->getDirectoryContent($source);
470
+            foreach ($content as $item) {
471
+                if ($item->getType() === FileInfo::TYPE_FOLDER) {
472
+                    $rootView->rmdir($item->getPath());
473
+                } else {
474
+                    $rootView->unlink($item->getPath());
475
+                }
476
+            }
477
+        } else {
478
+            throw new TransferOwnershipException("Could not transfer $source to $target");
479
+        }
480
+    }
481
+
482
+    /**
483
+     * @param string $targetLocation New location of the transfered node
484
+     * @param array<array{share: IShare, suffix: string}> $shares previously collected share information
485
+     */
486
+    private function restoreShares(
487
+        string $sourceUid,
488
+        string $destinationUid,
489
+        string $targetLocation,
490
+        array $shares,
491
+        OutputInterface $output,
492
+    ):void {
493
+        $output->writeln('Restoring shares ...');
494
+        $progress = new ProgressBar($output, count($shares));
495
+
496
+        foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
497
+            try {
498
+                $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
499
+                if ($share->getShareType() === IShare::TYPE_USER
500
+                    && $share->getSharedWith() === $destinationUid) {
501
+                    // Unmount the shares before deleting, so we don't try to get the storage later on.
502
+                    $shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
503
+                    if ($shareMountPoint) {
504
+                        $this->mountManager->removeMount($shareMountPoint->getMountPoint());
505
+                    }
506
+                    $this->shareManager->deleteShare($share);
507
+                } else {
508
+                    if ($share->getShareOwner() === $sourceUid) {
509
+                        $share->setShareOwner($destinationUid);
510
+                    }
511
+                    if ($share->getSharedBy() === $sourceUid) {
512
+                        $share->setSharedBy($destinationUid);
513
+                    }
514
+
515
+                    if ($share->getShareType() === IShare::TYPE_USER
516
+                        && !$this->userManager->userExists($share->getSharedWith())) {
517
+                        // stray share with deleted user
518
+                        $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
519
+                        $this->shareManager->deleteShare($share);
520
+                        continue;
521
+                    } else {
522
+                        // trigger refetching of the node so that the new owner and mountpoint are taken into account
523
+                        // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
524
+                        $this->userMountCache->clear();
525
+
526
+                        try {
527
+                            // Try to get the "old" id.
528
+                            // Normally the ID is preserved,
529
+                            // but for transferes between different storages the ID might change
530
+                            $newNodeId = $share->getNode()->getId();
531
+                        } catch (NotFoundException) {
532
+                            // ID has changed due to transfer between different storages
533
+                            // Try to get the new ID from the target path and suffix of the share
534
+                            $node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
535
+                            $newNodeId = $node->getId();
536
+                            $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
537
+                        }
538
+                        $share->setNodeId($newNodeId);
539
+
540
+                        $this->shareManager->updateShare($share, onlyValid: false);
541
+                    }
542
+                }
543
+            } catch (NotFoundException $e) {
544
+                $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
545
+            } catch (\Throwable $e) {
546
+                $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
547
+            }
548
+            $progress->advance();
549
+        }
550
+        $progress->finish();
551
+        $output->writeln('');
552
+    }
553
+
554
+    private function transferIncomingShares(string $sourceUid,
555
+        string $destinationUid,
556
+        array $sourceShares,
557
+        array $destinationShares,
558
+        OutputInterface $output,
559
+        string $path,
560
+        string $finalTarget,
561
+        bool $move): void {
562
+        $output->writeln('Restoring incoming shares ...');
563
+        $progress = new ProgressBar($output, count($sourceShares));
564
+        $prefix = "$destinationUid/files";
565
+        $finalShareTarget = '';
566
+        if (str_starts_with($finalTarget, $prefix)) {
567
+            $finalShareTarget = substr($finalTarget, strlen($prefix));
568
+        }
569
+        foreach ($sourceShares as $share) {
570
+            try {
571
+                // Only restore if share is in given path.
572
+                $pathToCheck = '/';
573
+                if (trim($path, '/') !== '') {
574
+                    $pathToCheck = '/' . trim($path) . '/';
575
+                }
576
+                if (!str_starts_with($share->getTarget(), $pathToCheck)) {
577
+                    continue;
578
+                }
579
+                $shareTarget = $share->getTarget();
580
+                $shareTarget = $finalShareTarget . $shareTarget;
581
+                if ($share->getShareType() === IShare::TYPE_USER
582
+                    && $share->getSharedBy() === $destinationUid) {
583
+                    $this->shareManager->deleteShare($share);
584
+                } elseif (isset($destinationShares[$share->getNodeId()])) {
585
+                    $destinationShare = $destinationShares[$share->getNodeId()];
586
+                    // Keep the share which has the most permissions and discard the other one.
587
+                    if ($destinationShare->getPermissions() < $share->getPermissions()) {
588
+                        $this->shareManager->deleteShare($destinationShare);
589
+                        $share->setSharedWith($destinationUid);
590
+                        // trigger refetching of the node so that the new owner and mountpoint are taken into account
591
+                        // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
592
+                        $this->userMountCache->clear();
593
+                        $share->setNodeId($share->getNode()->getId());
594
+                        $this->shareManager->updateShare($share);
595
+                        // The share is already transferred.
596
+                        $progress->advance();
597
+                        if ($move) {
598
+                            continue;
599
+                        }
600
+                        $share->setTarget($shareTarget);
601
+                        $this->shareManager->moveShare($share, $destinationUid);
602
+                        continue;
603
+                    }
604
+                    $this->shareManager->deleteShare($share);
605
+                } elseif ($share->getShareOwner() === $destinationUid) {
606
+                    $this->shareManager->deleteShare($share);
607
+                } else {
608
+                    $share->setSharedWith($destinationUid);
609
+                    $share->setNodeId($share->getNode()->getId());
610
+                    $this->shareManager->updateShare($share);
611
+                    // trigger refetching of the node so that the new owner and mountpoint are taken into account
612
+                    // otherwise the checks on the share update will fail due to the original node not being available in the new user scope
613
+                    $this->userMountCache->clear();
614
+                    // The share is already transferred.
615
+                    $progress->advance();
616
+                    if ($move) {
617
+                        continue;
618
+                    }
619
+                    $share->setTarget($shareTarget);
620
+                    $this->shareManager->moveShare($share, $destinationUid);
621
+                    continue;
622
+                }
623
+            } catch (NotFoundException $e) {
624
+                $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
625
+            } catch (\Throwable $e) {
626
+                $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
627
+            }
628
+            $progress->advance();
629
+        }
630
+        $progress->finish();
631
+        $output->writeln('');
632
+    }
633 633
 }
Please login to merge, or discard this patch.
Spacing   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -78,7 +78,7 @@  discard block
 block discarded – undo
78 78
 		$output = $output ?? new NullOutput();
79 79
 		$sourceUid = $sourceUser->getUID();
80 80
 		$destinationUid = $destinationUser->getUID();
81
-		$sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
81
+		$sourcePath = rtrim($sourceUid.'/files/'.$path, '/');
82 82
 
83 83
 		// If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready
84 84
 		if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0)
@@ -114,11 +114,11 @@  discard block
 block discarded – undo
114 114
 				}
115 115
 			}
116 116
 
117
-			$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
117
+			$finalTarget = "$destinationUid/files/".$this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date]));
118 118
 			try {
119 119
 				$view->verifyPath(dirname($finalTarget), basename($finalTarget));
120 120
 			} catch (InvalidPathException $e) {
121
-				$finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
121
+				$finalTarget = "$destinationUid/files/".$this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
122 122
 			}
123 123
 		}
124 124
 
@@ -188,7 +188,7 @@  discard block
 block discarded – undo
188 188
 			$move
189 189
 		);
190 190
 
191
-		$destinationPath = $finalTarget . '/' . $path;
191
+		$destinationPath = $finalTarget.'/'.$path;
192 192
 		// restore the shares
193 193
 		$this->restoreShares(
194 194
 			$sourceUid,
@@ -239,7 +239,7 @@  discard block
 block discarded – undo
239 239
 			throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1);
240 240
 		}
241 241
 		$size = $sourceFileInfo->getSize(false);
242
-		$freeSpace = $view->free_space($destinationUid . '/files/');
242
+		$freeSpace = $view->free_space($destinationUid.'/files/');
243 243
 		if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) {
244 244
 			throw new TransferOwnershipException('Target user does not have enough free space available.', 1);
245 245
 		}
@@ -260,7 +260,7 @@  discard block
 block discarded – undo
260 260
 				$encryptedFiles[] = $sourceFileInfo;
261 261
 			} else {
262 262
 				$this->walkFiles($view, $sourcePath,
263
-					function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
263
+					function(FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) {
264 264
 						if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
265 265
 							$mount = $fileInfo->getMountPoint();
266 266
 							// only analyze into folders from main storage,
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
 			$output->writeln('<error>Some files are encrypted - please decrypt them first.</error>');
298 298
 			foreach ($encryptedFiles as $encryptedFile) {
299 299
 				/** @var FileInfo $encryptedFile */
300
-				$output->writeln('  ' . $encryptedFile->getPath());
300
+				$output->writeln('  '.$encryptedFile->getPath());
301 301
 			}
302 302
 			throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
303 303
 		}
@@ -340,12 +340,12 @@  discard block
 block discarded – undo
340 340
 					break;
341 341
 				}
342 342
 				if ($path !== "$sourceUid/files") {
343
-					$sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
343
+					$sharePage = array_filter($sharePage, function(IShare $share) use ($view, $normalizedPath) {
344 344
 						try {
345 345
 							$sourceNode = $share->getNode();
346 346
 							$relativePath = $view->getRelativePath($sourceNode->getPath());
347 347
 
348
-							return str_starts_with($relativePath . '/', $normalizedPath . '/');
348
+							return str_starts_with($relativePath.'/', $normalizedPath.'/');
349 349
 						} catch (Exception $e) {
350 350
 							return false;
351 351
 						}
@@ -359,7 +359,7 @@  discard block
 block discarded – undo
359 359
 		$progress->finish();
360 360
 		$output->writeln('');
361 361
 
362
-		return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
362
+		return array_values(array_filter(array_map(function(IShare $share) use ($view, $normalizedPath, $output, $sourceUid) {
363 363
 			try {
364 364
 				$nodePath = $view->getRelativePath($share->getNode()->getPath());
365 365
 			} catch (NotFoundException $e) {
@@ -394,9 +394,9 @@  discard block
 block discarded – undo
394 394
 			}
395 395
 
396 396
 			if ($path !== null && $path !== "$sourceUid/files") {
397
-				$sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
397
+				$sharePage = array_filter($sharePage, static function(IShare $share) use ($sourceUid, $normalizedPath) {
398 398
 					try {
399
-						return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
399
+						return str_starts_with(Filesystem::normalizePath($sourceUid.'/files'.$share->getTarget().'/', false), $normalizedPath.'/');
400 400
 					} catch (Exception) {
401 401
 						return false;
402 402
 					}
@@ -433,7 +433,7 @@  discard block
 block discarded – undo
433 433
 		// Else only the content inside folder is transferred which is not correct.
434 434
 		if ($sourcePath !== "$sourceUid/files") {
435 435
 			$view->mkdir($finalTarget);
436
-			$finalTarget = $finalTarget . '/' . basename($sourcePath);
436
+			$finalTarget = $finalTarget.'/'.basename($sourcePath);
437 437
 		}
438 438
 		$sourceInfo = $view->getFileInfo($sourcePath);
439 439
 
@@ -451,7 +451,7 @@  discard block
 block discarded – undo
451 451
 			foreach ($nestedMounts as $mount) {
452 452
 				if ($mount->getMountProvider() === ConfigAdapter::class) {
453 453
 					$relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath));
454
-					$this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath);
454
+					$this->moveMountContents($view, $mount->getMountPoint(), $finalTarget.$relativePath);
455 455
 				}
456 456
 			}
457 457
 		}
@@ -495,11 +495,11 @@  discard block
 block discarded – undo
495 495
 
496 496
 		foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
497 497
 			try {
498
-				$output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
498
+				$output->writeln('Transfering share '.$share->getId().' of type '.$share->getShareType(), OutputInterface::VERBOSITY_VERBOSE);
499 499
 				if ($share->getShareType() === IShare::TYPE_USER
500 500
 					&& $share->getSharedWith() === $destinationUid) {
501 501
 					// Unmount the shares before deleting, so we don't try to get the storage later on.
502
-					$shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget());
502
+					$shareMountPoint = $this->mountManager->find('/'.$destinationUid.'/files'.$share->getTarget());
503 503
 					if ($shareMountPoint) {
504 504
 						$this->mountManager->removeMount($shareMountPoint->getMountPoint());
505 505
 					}
@@ -515,7 +515,7 @@  discard block
 block discarded – undo
515 515
 					if ($share->getShareType() === IShare::TYPE_USER
516 516
 						&& !$this->userManager->userExists($share->getSharedWith())) {
517 517
 						// stray share with deleted user
518
-						$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>');
518
+						$output->writeln('<error>Share with id '.$share->getId().' points at deleted user "'.$share->getSharedWith().'", deleting</error>');
519 519
 						$this->shareManager->deleteShare($share);
520 520
 						continue;
521 521
 					} else {
@@ -531,9 +531,9 @@  discard block
 block discarded – undo
531 531
 						} catch (NotFoundException) {
532 532
 							// ID has changed due to transfer between different storages
533 533
 							// Try to get the new ID from the target path and suffix of the share
534
-							$node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix));
534
+							$node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation.'/'.$suffix));
535 535
 							$newNodeId = $node->getId();
536
-							$output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
536
+							$output->writeln('Had to change node id to '.$newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE);
537 537
 						}
538 538
 						$share->setNodeId($newNodeId);
539 539
 
@@ -541,9 +541,9 @@  discard block
 block discarded – undo
541 541
 					}
542 542
 				}
543 543
 			} catch (NotFoundException $e) {
544
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
544
+				$output->writeln('<error>Share with id '.$share->getId().' points at deleted file, skipping</error>');
545 545
 			} catch (\Throwable $e) {
546
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>');
546
+				$output->writeln('<error>Could not restore share with id '.$share->getId().':'.$e->getMessage().' : '.$e->getTraceAsString().'</error>');
547 547
 			}
548 548
 			$progress->advance();
549 549
 		}
@@ -571,13 +571,13 @@  discard block
 block discarded – undo
571 571
 				// Only restore if share is in given path.
572 572
 				$pathToCheck = '/';
573 573
 				if (trim($path, '/') !== '') {
574
-					$pathToCheck = '/' . trim($path) . '/';
574
+					$pathToCheck = '/'.trim($path).'/';
575 575
 				}
576 576
 				if (!str_starts_with($share->getTarget(), $pathToCheck)) {
577 577
 					continue;
578 578
 				}
579 579
 				$shareTarget = $share->getTarget();
580
-				$shareTarget = $finalShareTarget . $shareTarget;
580
+				$shareTarget = $finalShareTarget.$shareTarget;
581 581
 				if ($share->getShareType() === IShare::TYPE_USER
582 582
 					&& $share->getSharedBy() === $destinationUid) {
583 583
 					$this->shareManager->deleteShare($share);
@@ -621,9 +621,9 @@  discard block
 block discarded – undo
621 621
 					continue;
622 622
 				}
623 623
 			} catch (NotFoundException $e) {
624
-				$output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>');
624
+				$output->writeln('<error>Share with id '.$share->getId().' points at deleted file, skipping</error>');
625 625
 			} catch (\Throwable $e) {
626
-				$output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>');
626
+				$output->writeln('<error>Could not restore share with id '.$share->getId().':'.$e->getTraceAsString().'</error>');
627 627
 			}
628 628
 			$progress->advance();
629 629
 		}
Please login to merge, or discard this patch.