Passed
Push — master ( 02a671...a719b4 )
by John
19:43 queued 14s
created
apps/files_trashbin/lib/Trashbin.php 2 patches
Indentation   +1084 added lines, -1084 removed lines patch added patch discarded remove patch
@@ -65,1088 +65,1088 @@
 block discarded – undo
65 65
 use Psr\Log\LoggerInterface;
66 66
 
67 67
 class Trashbin {
68
-	// unit: percentage; 50% of available disk space/quota
69
-	public const DEFAULTMAXSIZE = 50;
70
-
71
-	/**
72
-	 * Ensure we don't need to scan the file during the move to trash
73
-	 * by triggering the scan in the pre-hook
74
-	 *
75
-	 * @param array $params
76
-	 */
77
-	public static function ensureFileScannedHook($params) {
78
-		try {
79
-			self::getUidAndFilename($params['path']);
80
-		} catch (NotFoundException $e) {
81
-			// nothing to scan for non existing files
82
-		}
83
-	}
84
-
85
-	/**
86
-	 * get the UID of the owner of the file and the path to the file relative to
87
-	 * owners files folder
88
-	 *
89
-	 * @param string $filename
90
-	 * @return array
91
-	 * @throws \OC\User\NoUserException
92
-	 */
93
-	public static function getUidAndFilename($filename) {
94
-		$uid = Filesystem::getOwner($filename);
95
-		$userManager = \OC::$server->getUserManager();
96
-		// if the user with the UID doesn't exists, e.g. because the UID points
97
-		// to a remote user with a federated cloud ID we use the current logged-in
98
-		// user. We need a valid local user to move the file to the right trash bin
99
-		if (!$userManager->userExists($uid)) {
100
-			$uid = OC_User::getUser();
101
-		}
102
-		if (!$uid) {
103
-			// no owner, usually because of share link from ext storage
104
-			return [null, null];
105
-		}
106
-		Filesystem::initMountPoints($uid);
107
-		if ($uid !== OC_User::getUser()) {
108
-			$info = Filesystem::getFileInfo($filename);
109
-			$ownerView = new View('/' . $uid . '/files');
110
-			try {
111
-				$filename = $ownerView->getPath($info['fileid']);
112
-			} catch (NotFoundException $e) {
113
-				$filename = null;
114
-			}
115
-		}
116
-		return [$uid, $filename];
117
-	}
118
-
119
-	/**
120
-	 * get original location of files for user
121
-	 *
122
-	 * @param string $user
123
-	 * @return array (filename => array (timestamp => original location))
124
-	 */
125
-	public static function getLocations($user) {
126
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
127
-		$query->select('id', 'timestamp', 'location')
128
-			->from('files_trash')
129
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
130
-		$result = $query->executeQuery();
131
-		$array = [];
132
-		while ($row = $result->fetch()) {
133
-			if (isset($array[$row['id']])) {
134
-				$array[$row['id']][$row['timestamp']] = $row['location'];
135
-			} else {
136
-				$array[$row['id']] = [$row['timestamp'] => $row['location']];
137
-			}
138
-		}
139
-		$result->closeCursor();
140
-		return $array;
141
-	}
142
-
143
-	/**
144
-	 * get original location of file
145
-	 *
146
-	 * @param string $user
147
-	 * @param string $filename
148
-	 * @param string $timestamp
149
-	 * @return string original location
150
-	 */
151
-	public static function getLocation($user, $filename, $timestamp) {
152
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
153
-		$query->select('location')
154
-			->from('files_trash')
155
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)))
156
-			->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
157
-			->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
158
-
159
-		$result = $query->executeQuery();
160
-		$row = $result->fetch();
161
-		$result->closeCursor();
162
-
163
-		if (isset($row['location'])) {
164
-			return $row['location'];
165
-		} else {
166
-			return false;
167
-		}
168
-	}
169
-
170
-	private static function setUpTrash($user) {
171
-		$view = new View('/' . $user);
172
-		if (!$view->is_dir('files_trashbin')) {
173
-			$view->mkdir('files_trashbin');
174
-		}
175
-		if (!$view->is_dir('files_trashbin/files')) {
176
-			$view->mkdir('files_trashbin/files');
177
-		}
178
-		if (!$view->is_dir('files_trashbin/versions')) {
179
-			$view->mkdir('files_trashbin/versions');
180
-		}
181
-		if (!$view->is_dir('files_trashbin/keys')) {
182
-			$view->mkdir('files_trashbin/keys');
183
-		}
184
-	}
185
-
186
-
187
-	/**
188
-	 * copy file to owners trash
189
-	 *
190
-	 * @param string $sourcePath
191
-	 * @param string $owner
192
-	 * @param string $targetPath
193
-	 * @param $user
194
-	 * @param int $timestamp
195
-	 */
196
-	private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
197
-		self::setUpTrash($owner);
198
-
199
-		$targetFilename = basename($targetPath);
200
-		$targetLocation = dirname($targetPath);
201
-
202
-		$sourceFilename = basename($sourcePath);
203
-
204
-		$view = new View('/');
205
-
206
-		$target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
207
-		$source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
208
-		$free = $view->free_space($target);
209
-		$isUnknownOrUnlimitedFreeSpace = $free < 0;
210
-		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
211
-		if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
212
-			self::copy_recursive($source, $target, $view);
213
-		}
214
-
215
-
216
-		if ($view->file_exists($target)) {
217
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
218
-			$query->insert('files_trash')
219
-				->setValue('id', $query->createNamedParameter($targetFilename))
220
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
221
-				->setValue('location', $query->createNamedParameter($targetLocation))
222
-				->setValue('user', $query->createNamedParameter($user));
223
-			$result = $query->executeStatement();
224
-			if (!$result) {
225
-				\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
226
-			}
227
-		}
228
-	}
229
-
230
-
231
-	/**
232
-	 * move file to the trash bin
233
-	 *
234
-	 * @param string $file_path path to the deleted file/directory relative to the files root directory
235
-	 * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
236
-	 *
237
-	 * @return bool
238
-	 */
239
-	public static function move2trash($file_path, $ownerOnly = false) {
240
-		// get the user for which the filesystem is setup
241
-		$root = Filesystem::getRoot();
242
-		[, $user] = explode('/', $root);
243
-		[$owner, $ownerPath] = self::getUidAndFilename($file_path);
244
-
245
-		// if no owner found (ex: ext storage + share link), will use the current user's trashbin then
246
-		if (is_null($owner)) {
247
-			$owner = $user;
248
-			$ownerPath = $file_path;
249
-		}
250
-
251
-		$ownerView = new View('/' . $owner);
252
-
253
-		// file has been deleted in between
254
-		if (is_null($ownerPath) || $ownerPath === '') {
255
-			return true;
256
-		}
257
-
258
-		$sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
259
-
260
-		if ($sourceInfo === false) {
261
-			return true;
262
-		}
263
-
264
-		self::setUpTrash($user);
265
-		if ($owner !== $user) {
266
-			// also setup for owner
267
-			self::setUpTrash($owner);
268
-		}
269
-
270
-		$path_parts = pathinfo($ownerPath);
271
-
272
-		$filename = $path_parts['basename'];
273
-		$location = $path_parts['dirname'];
274
-		/** @var ITimeFactory $timeFactory */
275
-		$timeFactory = \OC::$server->query(ITimeFactory::class);
276
-		$timestamp = $timeFactory->getTime();
277
-
278
-		$lockingProvider = \OC::$server->getLockingProvider();
279
-
280
-		// disable proxy to prevent recursive calls
281
-		$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
282
-		$gotLock = false;
283
-
284
-		while (!$gotLock) {
285
-			try {
286
-				/** @var \OC\Files\Storage\Storage $trashStorage */
287
-				[$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
288
-
289
-				$trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
290
-				$gotLock = true;
291
-			} catch (LockedException $e) {
292
-				// a file with the same name is being deleted concurrently
293
-				// nudge the timestamp a bit to resolve the conflict
294
-
295
-				$timestamp = $timestamp + 1;
296
-
297
-				$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
298
-			}
299
-		}
300
-
301
-		$sourceStorage = $sourceInfo->getStorage();
302
-		$sourceInternalPath = $sourceInfo->getInternalPath();
303
-
304
-		if ($trashStorage->file_exists($trashInternalPath)) {
305
-			$trashStorage->unlink($trashInternalPath);
306
-		}
307
-
308
-		$configuredTrashbinSize = static::getConfiguredTrashbinSize($owner);
309
-		if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) {
310
-			return false;
311
-		}
312
-
313
-		$trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
314
-
315
-		try {
316
-			$moveSuccessful = true;
317
-
318
-			// when moving within the same object store, the cache update done above is enough to move the file
319
-			if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
320
-				$trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
321
-			}
322
-		} catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
323
-			$moveSuccessful = false;
324
-			if ($trashStorage->file_exists($trashInternalPath)) {
325
-				$trashStorage->unlink($trashInternalPath);
326
-			}
327
-			\OC::$server->get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
328
-		}
329
-
330
-		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
331
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
332
-				$sourceStorage->rmdir($sourceInternalPath);
333
-			} else {
334
-				$sourceStorage->unlink($sourceInternalPath);
335
-			}
336
-
337
-			if ($sourceStorage->file_exists($sourceInternalPath)) {
338
-				// undo the cache move
339
-				$sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
340
-			} else {
341
-				$trashStorage->getUpdater()->remove($trashInternalPath);
342
-			}
343
-			return false;
344
-		}
345
-
346
-		if ($moveSuccessful) {
347
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
348
-			$query->insert('files_trash')
349
-				->setValue('id', $query->createNamedParameter($filename))
350
-				->setValue('timestamp', $query->createNamedParameter($timestamp))
351
-				->setValue('location', $query->createNamedParameter($location))
352
-				->setValue('user', $query->createNamedParameter($owner));
353
-			$result = $query->executeStatement();
354
-			if (!$result) {
355
-				\OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
356
-			}
357
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
358
-				'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]);
359
-
360
-			self::retainVersions($filename, $owner, $ownerPath, $timestamp);
361
-
362
-			// if owner !== user we need to also add a copy to the users trash
363
-			if ($user !== $owner && $ownerOnly === false) {
364
-				self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
365
-			}
366
-		}
367
-
368
-		$trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
369
-
370
-		self::scheduleExpire($user);
371
-
372
-		// if owner !== user we also need to update the owners trash size
373
-		if ($owner !== $user) {
374
-			self::scheduleExpire($owner);
375
-		}
376
-
377
-		return $moveSuccessful;
378
-	}
379
-
380
-	private static function getConfiguredTrashbinSize(string $user): int|float {
381
-		$config = \OC::$server->get(IConfig::class);
382
-		$userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
383
-		if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
384
-			return \OCP\Util::numericToNumber($userTrashbinSize);
385
-		}
386
-		$systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
387
-		if (is_numeric($systemTrashbinSize)) {
388
-			return \OCP\Util::numericToNumber($systemTrashbinSize);
389
-		}
390
-		return -1;
391
-	}
392
-
393
-	/**
394
-	 * Move file versions to trash so that they can be restored later
395
-	 *
396
-	 * @param string $filename of deleted file
397
-	 * @param string $owner owner user id
398
-	 * @param string $ownerPath path relative to the owner's home storage
399
-	 * @param int $timestamp when the file was deleted
400
-	 */
401
-	private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
402
-		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) {
403
-			$user = OC_User::getUser();
404
-			$rootView = new View('/');
405
-
406
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
407
-				if ($owner !== $user) {
408
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
409
-				}
410
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
411
-			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
412
-				foreach ($versions as $v) {
413
-					if ($owner !== $user) {
414
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
415
-					}
416
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
417
-				}
418
-			}
419
-		}
420
-	}
421
-
422
-	/**
423
-	 * Move a file or folder on storage level
424
-	 *
425
-	 * @param View $view
426
-	 * @param string $source
427
-	 * @param string $target
428
-	 * @return bool
429
-	 */
430
-	private static function move(View $view, $source, $target) {
431
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
432
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
433
-		/** @var \OC\Files\Storage\Storage $targetStorage */
434
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
435
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
436
-
437
-		$result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
438
-		if ($result) {
439
-			$targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
440
-		}
441
-		return $result;
442
-	}
443
-
444
-	/**
445
-	 * Copy a file or folder on storage level
446
-	 *
447
-	 * @param View $view
448
-	 * @param string $source
449
-	 * @param string $target
450
-	 * @return bool
451
-	 */
452
-	private static function copy(View $view, $source, $target) {
453
-		/** @var \OC\Files\Storage\Storage $sourceStorage */
454
-		[$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
455
-		/** @var \OC\Files\Storage\Storage $targetStorage */
456
-		[$targetStorage, $targetInternalPath] = $view->resolvePath($target);
457
-		/** @var \OC\Files\Storage\Storage $ownerTrashStorage */
458
-
459
-		$result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
460
-		if ($result) {
461
-			$targetStorage->getUpdater()->update($targetInternalPath);
462
-		}
463
-		return $result;
464
-	}
465
-
466
-	/**
467
-	 * Restore a file or folder from trash bin
468
-	 *
469
-	 * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
470
-	 * including the timestamp suffix ".d12345678"
471
-	 * @param string $filename name of the file/folder
472
-	 * @param int $timestamp time when the file/folder was deleted
473
-	 *
474
-	 * @return bool true on success, false otherwise
475
-	 */
476
-	public static function restore($file, $filename, $timestamp) {
477
-		$user = OC_User::getUser();
478
-		$view = new View('/' . $user);
479
-
480
-		$location = '';
481
-		if ($timestamp) {
482
-			$location = self::getLocation($user, $filename, $timestamp);
483
-			if ($location === false) {
484
-				\OC::$server->get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
485
-			} else {
486
-				// if location no longer exists, restore file in the root directory
487
-				if ($location !== '/' &&
488
-					(!$view->is_dir('files/' . $location) ||
489
-						!$view->isCreatable('files/' . $location))
490
-				) {
491
-					$location = '';
492
-				}
493
-			}
494
-		}
495
-
496
-		// we need a  extension in case a file/dir with the same name already exists
497
-		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
498
-
499
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
500
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
501
-		if (!$view->file_exists($source)) {
502
-			return false;
503
-		}
504
-		$mtime = $view->filemtime($source);
505
-
506
-		// restore file
507
-		if (!$view->isCreatable(dirname($target))) {
508
-			throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
509
-		}
510
-		$restoreResult = $view->rename($source, $target);
511
-
512
-		// handle the restore result
513
-		if ($restoreResult) {
514
-			$fakeRoot = $view->getRoot();
515
-			$view->chroot('/' . $user . '/files');
516
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
517
-			$view->chroot($fakeRoot);
518
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
519
-				'trashPath' => Filesystem::normalizePath($file)]);
520
-
521
-			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
522
-
523
-			if ($timestamp) {
524
-				$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
525
-				$query->delete('files_trash')
526
-					->where($query->expr()->eq('user', $query->createNamedParameter($user)))
527
-					->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
528
-					->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
529
-				$query->executeStatement();
530
-			}
531
-
532
-			return true;
533
-		}
534
-
535
-		return false;
536
-	}
537
-
538
-	/**
539
-	 * restore versions from trash bin
540
-	 *
541
-	 * @param View $view file view
542
-	 * @param string $file complete path to file
543
-	 * @param string $filename name of file once it was deleted
544
-	 * @param string $uniqueFilename new file name to restore the file without overwriting existing files
545
-	 * @param string $location location if file
546
-	 * @param int $timestamp deletion time
547
-	 * @return false|null
548
-	 */
549
-	private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
550
-		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
551
-			$user = OC_User::getUser();
552
-			$rootView = new View('/');
553
-
554
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
555
-
556
-			[$owner, $ownerPath] = self::getUidAndFilename($target);
557
-
558
-			// file has been deleted in between
559
-			if (empty($ownerPath)) {
560
-				return false;
561
-			}
562
-
563
-			if ($timestamp) {
564
-				$versionedFile = $filename;
565
-			} else {
566
-				$versionedFile = $file;
567
-			}
568
-
569
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
570
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
571
-			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
572
-				foreach ($versions as $v) {
573
-					if ($timestamp) {
574
-						$rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
575
-					} else {
576
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
577
-					}
578
-				}
579
-			}
580
-		}
581
-	}
582
-
583
-	/**
584
-	 * delete all files from the trash
585
-	 */
586
-	public static function deleteAll() {
587
-		$user = OC_User::getUser();
588
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
589
-		$view = new View('/' . $user);
590
-		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
591
-
592
-		try {
593
-			$trash = $userRoot->get('files_trashbin');
594
-		} catch (NotFoundException $e) {
595
-			return false;
596
-		}
597
-
598
-		// Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
599
-		$filePaths = [];
600
-		foreach ($fileInfos as $fileInfo) {
601
-			$filePaths[] = $view->getRelativePath($fileInfo->getPath());
602
-		}
603
-		unset($fileInfos); // save memory
604
-
605
-		// Bulk PreDelete-Hook
606
-		\OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
607
-
608
-		// Single-File Hooks
609
-		foreach ($filePaths as $path) {
610
-			self::emitTrashbinPreDelete($path);
611
-		}
612
-
613
-		// actual file deletion
614
-		$trash->delete();
615
-
616
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
617
-		$query->delete('files_trash')
618
-			->where($query->expr()->eq('user', $query->createNamedParameter($user)));
619
-		$query->executeStatement();
620
-
621
-		// Bulk PostDelete-Hook
622
-		\OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
623
-
624
-		// Single-File Hooks
625
-		foreach ($filePaths as $path) {
626
-			self::emitTrashbinPostDelete($path);
627
-		}
628
-
629
-		$trash = $userRoot->newFolder('files_trashbin');
630
-		$trash->newFolder('files');
631
-
632
-		return true;
633
-	}
634
-
635
-	/**
636
-	 * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
637
-	 *
638
-	 * @param string $path
639
-	 */
640
-	protected static function emitTrashbinPreDelete($path) {
641
-		\OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
642
-	}
643
-
644
-	/**
645
-	 * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
646
-	 *
647
-	 * @param string $path
648
-	 */
649
-	protected static function emitTrashbinPostDelete($path) {
650
-		\OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
651
-	}
652
-
653
-	/**
654
-	 * delete file from trash bin permanently
655
-	 *
656
-	 * @param string $filename path to the file
657
-	 * @param string $user
658
-	 * @param int $timestamp of deletion time
659
-	 *
660
-	 * @return int|float size of deleted files
661
-	 */
662
-	public static function delete($filename, $user, $timestamp = null) {
663
-		$userRoot = \OC::$server->getUserFolder($user)->getParent();
664
-		$view = new View('/' . $user);
665
-		$size = 0;
666
-
667
-		if ($timestamp) {
668
-			$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
669
-			$query->delete('files_trash')
670
-				->where($query->expr()->eq('user', $query->createNamedParameter($user)))
671
-				->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
672
-				->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
673
-			$query->executeStatement();
674
-
675
-			$file = static::getTrashFilename($filename, $timestamp);
676
-		} else {
677
-			$file = $filename;
678
-		}
679
-
680
-		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
681
-
682
-		try {
683
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
684
-		} catch (NotFoundException $e) {
685
-			return $size;
686
-		}
687
-
688
-		if ($node instanceof Folder) {
689
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
690
-		} elseif ($node instanceof File) {
691
-			$size += $view->filesize('/files_trashbin/files/' . $file);
692
-		}
693
-
694
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
695
-		$node->delete();
696
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
697
-
698
-		return $size;
699
-	}
700
-
701
-	/**
702
-	 * @param string $file
703
-	 * @param string $filename
704
-	 * @param ?int $timestamp
705
-	 */
706
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
707
-		$size = 0;
708
-		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
709
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
710
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
711
-				$view->unlink('files_trashbin/versions/' . $file);
712
-			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
713
-				foreach ($versions as $v) {
714
-					if ($timestamp) {
715
-						$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
716
-						$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
717
-					} else {
718
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
719
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
720
-					}
721
-				}
722
-			}
723
-		}
724
-		return $size;
725
-	}
726
-
727
-	/**
728
-	 * check to see whether a file exists in trashbin
729
-	 *
730
-	 * @param string $filename path to the file
731
-	 * @param int $timestamp of deletion time
732
-	 * @return bool true if file exists, otherwise false
733
-	 */
734
-	public static function file_exists($filename, $timestamp = null) {
735
-		$user = OC_User::getUser();
736
-		$view = new View('/' . $user);
737
-
738
-		if ($timestamp) {
739
-			$filename = static::getTrashFilename($filename, $timestamp);
740
-		}
741
-
742
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
743
-		return $view->file_exists($target);
744
-	}
745
-
746
-	/**
747
-	 * deletes used space for trash bin in db if user was deleted
748
-	 *
749
-	 * @param string $uid id of deleted user
750
-	 * @return bool result of db delete operation
751
-	 */
752
-	public static function deleteUser($uid) {
753
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
754
-		$query->delete('files_trash')
755
-			->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
756
-		return (bool) $query->executeStatement();
757
-	}
758
-
759
-	/**
760
-	 * calculate remaining free space for trash bin
761
-	 *
762
-	 * @param int|float $trashbinSize current size of the trash bin
763
-	 * @param string $user
764
-	 * @return int|float available free space for trash bin
765
-	 */
766
-	private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
767
-		$configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
768
-		if ($configuredTrashbinSize > -1) {
769
-			return $configuredTrashbinSize - $trashbinSize;
770
-		}
771
-
772
-		$userObject = \OC::$server->getUserManager()->get($user);
773
-		if (is_null($userObject)) {
774
-			return 0;
775
-		}
776
-		$softQuota = true;
777
-		$quota = $userObject->getQuota();
778
-		if ($quota === null || $quota === 'none') {
779
-			$quota = Filesystem::free_space('/');
780
-			$softQuota = false;
781
-			// inf or unknown free space
782
-			if ($quota < 0) {
783
-				$quota = PHP_INT_MAX;
784
-			}
785
-		} else {
786
-			$quota = \OCP\Util::computerFileSize($quota);
787
-			// invalid quota
788
-			if ($quota === false) {
789
-				$quota = PHP_INT_MAX;
790
-			}
791
-		}
792
-
793
-		// calculate available space for trash bin
794
-		// subtract size of files and current trash bin size from quota
795
-		if ($softQuota) {
796
-			$userFolder = \OC::$server->getUserFolder($user);
797
-			if (is_null($userFolder)) {
798
-				return 0;
799
-			}
800
-			$free = $quota - $userFolder->getSize(false); // remaining free space for user
801
-			if ($free > 0) {
802
-				$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
803
-			} else {
804
-				$availableSpace = $free - $trashbinSize;
805
-			}
806
-		} else {
807
-			$availableSpace = $quota;
808
-		}
809
-
810
-		return \OCP\Util::numericToNumber($availableSpace);
811
-	}
812
-
813
-	/**
814
-	 * resize trash bin if necessary after a new file was added to Nextcloud
815
-	 *
816
-	 * @param string $user user id
817
-	 */
818
-	public static function resizeTrash($user) {
819
-		$size = self::getTrashbinSize($user);
820
-
821
-		$freeSpace = self::calculateFreeSpace($size, $user);
822
-
823
-		if ($freeSpace < 0) {
824
-			self::scheduleExpire($user);
825
-		}
826
-	}
827
-
828
-	/**
829
-	 * clean up the trash bin
830
-	 *
831
-	 * @param string $user
832
-	 */
833
-	public static function expire($user) {
834
-		$trashBinSize = self::getTrashbinSize($user);
835
-		$availableSpace = self::calculateFreeSpace($trashBinSize, $user);
836
-
837
-		$dirContent = Helper::getTrashFiles('/', $user, 'mtime');
838
-
839
-		// delete all files older then $retention_obligation
840
-		[$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
841
-
842
-		$availableSpace += $delSize;
843
-
844
-		// delete files from trash until we meet the trash bin size limit again
845
-		self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
846
-	}
847
-
848
-	/**
849
-	 * @param string $user
850
-	 */
851
-	private static function scheduleExpire($user) {
852
-		// let the admin disable auto expire
853
-		/** @var Application $application */
854
-		$application = \OC::$server->query(Application::class);
855
-		$expiration = $application->getContainer()->query('Expiration');
856
-		if ($expiration->isEnabled()) {
857
-			\OC::$server->getCommandBus()->push(new Expire($user));
858
-		}
859
-	}
860
-
861
-	/**
862
-	 * if the size limit for the trash bin is reached, we delete the oldest
863
-	 * files in the trash bin until we meet the limit again
864
-	 *
865
-	 * @param array $files
866
-	 * @param string $user
867
-	 * @param int|float $availableSpace available disc space
868
-	 * @return int|float size of deleted files
869
-	 */
870
-	protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
871
-		/** @var Application $application */
872
-		$application = \OC::$server->query(Application::class);
873
-		$expiration = $application->getContainer()->query('Expiration');
874
-		$size = 0;
875
-
876
-		if ($availableSpace < 0) {
877
-			foreach ($files as $file) {
878
-				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
879
-					$tmp = self::delete($file['name'], $user, $file['mtime']);
880
-					\OC::$server->get(LoggerInterface::class)->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
881
-					$availableSpace += $tmp;
882
-					$size += $tmp;
883
-				} else {
884
-					break;
885
-				}
886
-			}
887
-		}
888
-		return $size;
889
-	}
890
-
891
-	/**
892
-	 * delete files older then max storage time
893
-	 *
894
-	 * @param array $files list of files sorted by mtime
895
-	 * @param string $user
896
-	 * @return array{int|float, int} size of deleted files and number of deleted files
897
-	 */
898
-	public static function deleteExpiredFiles($files, $user) {
899
-		/** @var Expiration $expiration */
900
-		$expiration = \OC::$server->query(Expiration::class);
901
-		$size = 0;
902
-		$count = 0;
903
-		foreach ($files as $file) {
904
-			$timestamp = $file['mtime'];
905
-			$filename = $file['name'];
906
-			if ($expiration->isExpired($timestamp)) {
907
-				try {
908
-					$size += self::delete($filename, $user, $timestamp);
909
-					$count++;
910
-				} catch (\OCP\Files\NotPermittedException $e) {
911
-					\OC::$server->get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed.',
912
-						[
913
-							'exception' => $e,
914
-							'app' => 'files_trashbin',
915
-						]
916
-					);
917
-				}
918
-				\OC::$server->get(LoggerInterface::class)->info(
919
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
920
-					['app' => 'files_trashbin']
921
-				);
922
-			} else {
923
-				break;
924
-			}
925
-		}
926
-
927
-		return [$size, $count];
928
-	}
929
-
930
-	/**
931
-	 * recursive copy to copy a whole directory
932
-	 *
933
-	 * @param string $source source path, relative to the users files directory
934
-	 * @param string $destination destination path relative to the users root directory
935
-	 * @param View $view file view for the users root directory
936
-	 * @return int|float
937
-	 * @throws Exceptions\CopyRecursiveException
938
-	 */
939
-	private static function copy_recursive($source, $destination, View $view): int|float {
940
-		$size = 0;
941
-		if ($view->is_dir($source)) {
942
-			$view->mkdir($destination);
943
-			$view->touch($destination, $view->filemtime($source));
944
-			foreach ($view->getDirectoryContent($source) as $i) {
945
-				$pathDir = $source . '/' . $i['name'];
946
-				if ($view->is_dir($pathDir)) {
947
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
948
-				} else {
949
-					$size += $view->filesize($pathDir);
950
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
951
-					if (!$result) {
952
-						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
953
-					}
954
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
955
-				}
956
-			}
957
-		} else {
958
-			$size += $view->filesize($source);
959
-			$result = $view->copy($source, $destination);
960
-			if (!$result) {
961
-				throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
962
-			}
963
-			$view->touch($destination, $view->filemtime($source));
964
-		}
965
-		return $size;
966
-	}
967
-
968
-	/**
969
-	 * find all versions which belong to the file we want to restore
970
-	 *
971
-	 * @param string $filename name of the file which should be restored
972
-	 * @param int $timestamp timestamp when the file was deleted
973
-	 */
974
-	private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
975
-		$view = new View('/' . $user . '/files_trashbin/versions');
976
-		$versions = [];
977
-
978
-		/** @var \OC\Files\Storage\Storage $storage */
979
-		[$storage,] = $view->resolvePath('/');
980
-
981
-		$pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
982
-		if ($timestamp) {
983
-			// fetch for old versions
984
-			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
985
-			$pattern .= '.v%.d' . $escapedTimestamp;
986
-			$offset = -strlen($escapedTimestamp) - 2;
987
-		} else {
988
-			$pattern .= '.v%';
989
-		}
990
-
991
-		// Manually fetch all versions from the file cache to be able to filter them by their parent
992
-		$cache = $storage->getCache('');
993
-		$query = new CacheQueryBuilder(
994
-			\OC::$server->getDatabaseConnection(),
995
-			\OC::$server->getSystemConfig(),
996
-			\OC::$server->get(LoggerInterface::class)
997
-		);
998
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
999
-		$parentId = $cache->getId($normalizedParentPath);
1000
-		if ($parentId === -1) {
1001
-			return [];
1002
-		}
1003
-
1004
-		$query->selectFileCache()
1005
-			->whereStorageId($cache->getNumericStorageId())
1006
-			->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1007
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1008
-
1009
-		$result = $query->executeQuery();
1010
-		$entries = $result->fetchAll();
1011
-		$result->closeCursor();
1012
-
1013
-		/** @var CacheEntry[] $matches */
1014
-		$matches = array_map(function (array $data) {
1015
-			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1016
-		}, $entries);
1017
-
1018
-		foreach ($matches as $ma) {
1019
-			if ($timestamp) {
1020
-				$parts = explode('.v', substr($ma['path'], 0, $offset));
1021
-				$versions[] = end($parts);
1022
-			} else {
1023
-				$parts = explode('.v', $ma['path']);
1024
-				$versions[] = end($parts);
1025
-			}
1026
-		}
1027
-
1028
-		return $versions;
1029
-	}
1030
-
1031
-	/**
1032
-	 * find unique extension for restored file if a file with the same name already exists
1033
-	 *
1034
-	 * @param string $location where the file should be restored
1035
-	 * @param string $filename name of the file
1036
-	 * @param View $view filesystem view relative to users root directory
1037
-	 * @return string with unique extension
1038
-	 */
1039
-	private static function getUniqueFilename($location, $filename, View $view) {
1040
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
1041
-		$name = pathinfo($filename, PATHINFO_FILENAME);
1042
-		$l = \OC::$server->getL10N('files_trashbin');
1043
-
1044
-		$location = '/' . trim($location, '/');
1045
-
1046
-		// if extension is not empty we set a dot in front of it
1047
-		if ($ext !== '') {
1048
-			$ext = '.' . $ext;
1049
-		}
1050
-
1051
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1052
-			$i = 2;
1053
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1054
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1055
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1056
-				$i++;
1057
-			}
1058
-
1059
-			return $uniqueName;
1060
-		}
1061
-
1062
-		return $filename;
1063
-	}
1064
-
1065
-	/**
1066
-	 * get the size from a given root folder
1067
-	 *
1068
-	 * @param View $view file view on the root folder
1069
-	 * @return int|float size of the folder
1070
-	 */
1071
-	private static function calculateSize(View $view): int|float {
1072
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1073
-		if (!file_exists($root)) {
1074
-			return 0;
1075
-		}
1076
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1077
-		$size = 0;
1078
-
1079
-		/**
1080
-		 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1081
-		 * This bug is fixed in PHP 5.5.9 or before
1082
-		 * See #8376
1083
-		 */
1084
-		$iterator->rewind();
1085
-		while ($iterator->valid()) {
1086
-			$path = $iterator->current();
1087
-			$relpath = substr($path, strlen($root) - 1);
1088
-			if (!$view->is_dir($relpath)) {
1089
-				$size += $view->filesize($relpath);
1090
-			}
1091
-			$iterator->next();
1092
-		}
1093
-		return $size;
1094
-	}
1095
-
1096
-	/**
1097
-	 * get current size of trash bin from a given user
1098
-	 *
1099
-	 * @param string $user user who owns the trash bin
1100
-	 * @return int|float trash bin size
1101
-	 */
1102
-	private static function getTrashbinSize(string $user): int|float {
1103
-		$view = new View('/' . $user);
1104
-		$fileInfo = $view->getFileInfo('/files_trashbin');
1105
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1106
-	}
1107
-
1108
-	/**
1109
-	 * check if trash bin is empty for a given user
1110
-	 *
1111
-	 * @param string $user
1112
-	 * @return bool
1113
-	 */
1114
-	public static function isEmpty($user) {
1115
-		$view = new View('/' . $user . '/files_trashbin');
1116
-		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1117
-			while ($file = readdir($dh)) {
1118
-				if (!Filesystem::isIgnoredDir($file)) {
1119
-					return false;
1120
-				}
1121
-			}
1122
-		}
1123
-		return true;
1124
-	}
1125
-
1126
-	/**
1127
-	 * @param $path
1128
-	 * @return string
1129
-	 */
1130
-	public static function preview_icon($path) {
1131
-		return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1132
-	}
1133
-
1134
-	/**
1135
-	 * Return the filename used in the trash bin
1136
-	 */
1137
-	public static function getTrashFilename(string $filename, int $timestamp): string {
1138
-		$trashFilename = $filename . '.d' . $timestamp;
1139
-		$length = strlen($trashFilename);
1140
-		// oc_filecache `name` column has a limit of 250 chars
1141
-		$maxLength = 250;
1142
-		if ($length > $maxLength) {
1143
-			$trashFilename = substr_replace(
1144
-				$trashFilename,
1145
-				'',
1146
-				$maxLength / 2,
1147
-				$length - $maxLength
1148
-			);
1149
-		}
1150
-		return $trashFilename;
1151
-	}
68
+    // unit: percentage; 50% of available disk space/quota
69
+    public const DEFAULTMAXSIZE = 50;
70
+
71
+    /**
72
+     * Ensure we don't need to scan the file during the move to trash
73
+     * by triggering the scan in the pre-hook
74
+     *
75
+     * @param array $params
76
+     */
77
+    public static function ensureFileScannedHook($params) {
78
+        try {
79
+            self::getUidAndFilename($params['path']);
80
+        } catch (NotFoundException $e) {
81
+            // nothing to scan for non existing files
82
+        }
83
+    }
84
+
85
+    /**
86
+     * get the UID of the owner of the file and the path to the file relative to
87
+     * owners files folder
88
+     *
89
+     * @param string $filename
90
+     * @return array
91
+     * @throws \OC\User\NoUserException
92
+     */
93
+    public static function getUidAndFilename($filename) {
94
+        $uid = Filesystem::getOwner($filename);
95
+        $userManager = \OC::$server->getUserManager();
96
+        // if the user with the UID doesn't exists, e.g. because the UID points
97
+        // to a remote user with a federated cloud ID we use the current logged-in
98
+        // user. We need a valid local user to move the file to the right trash bin
99
+        if (!$userManager->userExists($uid)) {
100
+            $uid = OC_User::getUser();
101
+        }
102
+        if (!$uid) {
103
+            // no owner, usually because of share link from ext storage
104
+            return [null, null];
105
+        }
106
+        Filesystem::initMountPoints($uid);
107
+        if ($uid !== OC_User::getUser()) {
108
+            $info = Filesystem::getFileInfo($filename);
109
+            $ownerView = new View('/' . $uid . '/files');
110
+            try {
111
+                $filename = $ownerView->getPath($info['fileid']);
112
+            } catch (NotFoundException $e) {
113
+                $filename = null;
114
+            }
115
+        }
116
+        return [$uid, $filename];
117
+    }
118
+
119
+    /**
120
+     * get original location of files for user
121
+     *
122
+     * @param string $user
123
+     * @return array (filename => array (timestamp => original location))
124
+     */
125
+    public static function getLocations($user) {
126
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
127
+        $query->select('id', 'timestamp', 'location')
128
+            ->from('files_trash')
129
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
130
+        $result = $query->executeQuery();
131
+        $array = [];
132
+        while ($row = $result->fetch()) {
133
+            if (isset($array[$row['id']])) {
134
+                $array[$row['id']][$row['timestamp']] = $row['location'];
135
+            } else {
136
+                $array[$row['id']] = [$row['timestamp'] => $row['location']];
137
+            }
138
+        }
139
+        $result->closeCursor();
140
+        return $array;
141
+    }
142
+
143
+    /**
144
+     * get original location of file
145
+     *
146
+     * @param string $user
147
+     * @param string $filename
148
+     * @param string $timestamp
149
+     * @return string original location
150
+     */
151
+    public static function getLocation($user, $filename, $timestamp) {
152
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
153
+        $query->select('location')
154
+            ->from('files_trash')
155
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
156
+            ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
157
+            ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
158
+
159
+        $result = $query->executeQuery();
160
+        $row = $result->fetch();
161
+        $result->closeCursor();
162
+
163
+        if (isset($row['location'])) {
164
+            return $row['location'];
165
+        } else {
166
+            return false;
167
+        }
168
+    }
169
+
170
+    private static function setUpTrash($user) {
171
+        $view = new View('/' . $user);
172
+        if (!$view->is_dir('files_trashbin')) {
173
+            $view->mkdir('files_trashbin');
174
+        }
175
+        if (!$view->is_dir('files_trashbin/files')) {
176
+            $view->mkdir('files_trashbin/files');
177
+        }
178
+        if (!$view->is_dir('files_trashbin/versions')) {
179
+            $view->mkdir('files_trashbin/versions');
180
+        }
181
+        if (!$view->is_dir('files_trashbin/keys')) {
182
+            $view->mkdir('files_trashbin/keys');
183
+        }
184
+    }
185
+
186
+
187
+    /**
188
+     * copy file to owners trash
189
+     *
190
+     * @param string $sourcePath
191
+     * @param string $owner
192
+     * @param string $targetPath
193
+     * @param $user
194
+     * @param int $timestamp
195
+     */
196
+    private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
197
+        self::setUpTrash($owner);
198
+
199
+        $targetFilename = basename($targetPath);
200
+        $targetLocation = dirname($targetPath);
201
+
202
+        $sourceFilename = basename($sourcePath);
203
+
204
+        $view = new View('/');
205
+
206
+        $target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
207
+        $source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
208
+        $free = $view->free_space($target);
209
+        $isUnknownOrUnlimitedFreeSpace = $free < 0;
210
+        $isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
211
+        if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
212
+            self::copy_recursive($source, $target, $view);
213
+        }
214
+
215
+
216
+        if ($view->file_exists($target)) {
217
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
218
+            $query->insert('files_trash')
219
+                ->setValue('id', $query->createNamedParameter($targetFilename))
220
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
221
+                ->setValue('location', $query->createNamedParameter($targetLocation))
222
+                ->setValue('user', $query->createNamedParameter($user));
223
+            $result = $query->executeStatement();
224
+            if (!$result) {
225
+                \OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
226
+            }
227
+        }
228
+    }
229
+
230
+
231
+    /**
232
+     * move file to the trash bin
233
+     *
234
+     * @param string $file_path path to the deleted file/directory relative to the files root directory
235
+     * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
236
+     *
237
+     * @return bool
238
+     */
239
+    public static function move2trash($file_path, $ownerOnly = false) {
240
+        // get the user for which the filesystem is setup
241
+        $root = Filesystem::getRoot();
242
+        [, $user] = explode('/', $root);
243
+        [$owner, $ownerPath] = self::getUidAndFilename($file_path);
244
+
245
+        // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
246
+        if (is_null($owner)) {
247
+            $owner = $user;
248
+            $ownerPath = $file_path;
249
+        }
250
+
251
+        $ownerView = new View('/' . $owner);
252
+
253
+        // file has been deleted in between
254
+        if (is_null($ownerPath) || $ownerPath === '') {
255
+            return true;
256
+        }
257
+
258
+        $sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
259
+
260
+        if ($sourceInfo === false) {
261
+            return true;
262
+        }
263
+
264
+        self::setUpTrash($user);
265
+        if ($owner !== $user) {
266
+            // also setup for owner
267
+            self::setUpTrash($owner);
268
+        }
269
+
270
+        $path_parts = pathinfo($ownerPath);
271
+
272
+        $filename = $path_parts['basename'];
273
+        $location = $path_parts['dirname'];
274
+        /** @var ITimeFactory $timeFactory */
275
+        $timeFactory = \OC::$server->query(ITimeFactory::class);
276
+        $timestamp = $timeFactory->getTime();
277
+
278
+        $lockingProvider = \OC::$server->getLockingProvider();
279
+
280
+        // disable proxy to prevent recursive calls
281
+        $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
282
+        $gotLock = false;
283
+
284
+        while (!$gotLock) {
285
+            try {
286
+                /** @var \OC\Files\Storage\Storage $trashStorage */
287
+                [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
288
+
289
+                $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
290
+                $gotLock = true;
291
+            } catch (LockedException $e) {
292
+                // a file with the same name is being deleted concurrently
293
+                // nudge the timestamp a bit to resolve the conflict
294
+
295
+                $timestamp = $timestamp + 1;
296
+
297
+                $trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
298
+            }
299
+        }
300
+
301
+        $sourceStorage = $sourceInfo->getStorage();
302
+        $sourceInternalPath = $sourceInfo->getInternalPath();
303
+
304
+        if ($trashStorage->file_exists($trashInternalPath)) {
305
+            $trashStorage->unlink($trashInternalPath);
306
+        }
307
+
308
+        $configuredTrashbinSize = static::getConfiguredTrashbinSize($owner);
309
+        if ($configuredTrashbinSize >= 0 && $sourceInfo->getSize() >= $configuredTrashbinSize) {
310
+            return false;
311
+        }
312
+
313
+        $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
314
+
315
+        try {
316
+            $moveSuccessful = true;
317
+
318
+            // when moving within the same object store, the cache update done above is enough to move the file
319
+            if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
320
+                $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
321
+            }
322
+        } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
323
+            $moveSuccessful = false;
324
+            if ($trashStorage->file_exists($trashInternalPath)) {
325
+                $trashStorage->unlink($trashInternalPath);
326
+            }
327
+            \OC::$server->get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
328
+        }
329
+
330
+        if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
331
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
332
+                $sourceStorage->rmdir($sourceInternalPath);
333
+            } else {
334
+                $sourceStorage->unlink($sourceInternalPath);
335
+            }
336
+
337
+            if ($sourceStorage->file_exists($sourceInternalPath)) {
338
+                // undo the cache move
339
+                $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath);
340
+            } else {
341
+                $trashStorage->getUpdater()->remove($trashInternalPath);
342
+            }
343
+            return false;
344
+        }
345
+
346
+        if ($moveSuccessful) {
347
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
348
+            $query->insert('files_trash')
349
+                ->setValue('id', $query->createNamedParameter($filename))
350
+                ->setValue('timestamp', $query->createNamedParameter($timestamp))
351
+                ->setValue('location', $query->createNamedParameter($location))
352
+                ->setValue('user', $query->createNamedParameter($owner));
353
+            $result = $query->executeStatement();
354
+            if (!$result) {
355
+                \OC::$server->get(LoggerInterface::class)->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
356
+            }
357
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
358
+                'trashPath' => Filesystem::normalizePath(static::getTrashFilename($filename, $timestamp))]);
359
+
360
+            self::retainVersions($filename, $owner, $ownerPath, $timestamp);
361
+
362
+            // if owner !== user we need to also add a copy to the users trash
363
+            if ($user !== $owner && $ownerOnly === false) {
364
+                self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
365
+            }
366
+        }
367
+
368
+        $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
369
+
370
+        self::scheduleExpire($user);
371
+
372
+        // if owner !== user we also need to update the owners trash size
373
+        if ($owner !== $user) {
374
+            self::scheduleExpire($owner);
375
+        }
376
+
377
+        return $moveSuccessful;
378
+    }
379
+
380
+    private static function getConfiguredTrashbinSize(string $user): int|float {
381
+        $config = \OC::$server->get(IConfig::class);
382
+        $userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
383
+        if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
384
+            return \OCP\Util::numericToNumber($userTrashbinSize);
385
+        }
386
+        $systemTrashbinSize = $config->getAppValue('files_trashbin', 'trashbin_size', '-1');
387
+        if (is_numeric($systemTrashbinSize)) {
388
+            return \OCP\Util::numericToNumber($systemTrashbinSize);
389
+        }
390
+        return -1;
391
+    }
392
+
393
+    /**
394
+     * Move file versions to trash so that they can be restored later
395
+     *
396
+     * @param string $filename of deleted file
397
+     * @param string $owner owner user id
398
+     * @param string $ownerPath path relative to the owner's home storage
399
+     * @param int $timestamp when the file was deleted
400
+     */
401
+    private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
402
+        if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions') && !empty($ownerPath)) {
403
+            $user = OC_User::getUser();
404
+            $rootView = new View('/');
405
+
406
+            if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
407
+                if ($owner !== $user) {
408
+                    self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
409
+                }
410
+                self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
411
+            } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
412
+                foreach ($versions as $v) {
413
+                    if ($owner !== $user) {
414
+                        self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
415
+                    }
416
+                    self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
417
+                }
418
+            }
419
+        }
420
+    }
421
+
422
+    /**
423
+     * Move a file or folder on storage level
424
+     *
425
+     * @param View $view
426
+     * @param string $source
427
+     * @param string $target
428
+     * @return bool
429
+     */
430
+    private static function move(View $view, $source, $target) {
431
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
432
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
433
+        /** @var \OC\Files\Storage\Storage $targetStorage */
434
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
435
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
436
+
437
+        $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
438
+        if ($result) {
439
+            $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
440
+        }
441
+        return $result;
442
+    }
443
+
444
+    /**
445
+     * Copy a file or folder on storage level
446
+     *
447
+     * @param View $view
448
+     * @param string $source
449
+     * @param string $target
450
+     * @return bool
451
+     */
452
+    private static function copy(View $view, $source, $target) {
453
+        /** @var \OC\Files\Storage\Storage $sourceStorage */
454
+        [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
455
+        /** @var \OC\Files\Storage\Storage $targetStorage */
456
+        [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
457
+        /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
458
+
459
+        $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
460
+        if ($result) {
461
+            $targetStorage->getUpdater()->update($targetInternalPath);
462
+        }
463
+        return $result;
464
+    }
465
+
466
+    /**
467
+     * Restore a file or folder from trash bin
468
+     *
469
+     * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
470
+     * including the timestamp suffix ".d12345678"
471
+     * @param string $filename name of the file/folder
472
+     * @param int $timestamp time when the file/folder was deleted
473
+     *
474
+     * @return bool true on success, false otherwise
475
+     */
476
+    public static function restore($file, $filename, $timestamp) {
477
+        $user = OC_User::getUser();
478
+        $view = new View('/' . $user);
479
+
480
+        $location = '';
481
+        if ($timestamp) {
482
+            $location = self::getLocation($user, $filename, $timestamp);
483
+            if ($location === false) {
484
+                \OC::$server->get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
485
+            } else {
486
+                // if location no longer exists, restore file in the root directory
487
+                if ($location !== '/' &&
488
+                    (!$view->is_dir('files/' . $location) ||
489
+                        !$view->isCreatable('files/' . $location))
490
+                ) {
491
+                    $location = '';
492
+                }
493
+            }
494
+        }
495
+
496
+        // we need a  extension in case a file/dir with the same name already exists
497
+        $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
498
+
499
+        $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
500
+        $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
501
+        if (!$view->file_exists($source)) {
502
+            return false;
503
+        }
504
+        $mtime = $view->filemtime($source);
505
+
506
+        // restore file
507
+        if (!$view->isCreatable(dirname($target))) {
508
+            throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
509
+        }
510
+        $restoreResult = $view->rename($source, $target);
511
+
512
+        // handle the restore result
513
+        if ($restoreResult) {
514
+            $fakeRoot = $view->getRoot();
515
+            $view->chroot('/' . $user . '/files');
516
+            $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
517
+            $view->chroot($fakeRoot);
518
+            \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
519
+                'trashPath' => Filesystem::normalizePath($file)]);
520
+
521
+            self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
522
+
523
+            if ($timestamp) {
524
+                $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
525
+                $query->delete('files_trash')
526
+                    ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
527
+                    ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
528
+                    ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
529
+                $query->executeStatement();
530
+            }
531
+
532
+            return true;
533
+        }
534
+
535
+        return false;
536
+    }
537
+
538
+    /**
539
+     * restore versions from trash bin
540
+     *
541
+     * @param View $view file view
542
+     * @param string $file complete path to file
543
+     * @param string $filename name of file once it was deleted
544
+     * @param string $uniqueFilename new file name to restore the file without overwriting existing files
545
+     * @param string $location location if file
546
+     * @param int $timestamp deletion time
547
+     * @return false|null
548
+     */
549
+    private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
550
+        if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
551
+            $user = OC_User::getUser();
552
+            $rootView = new View('/');
553
+
554
+            $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
555
+
556
+            [$owner, $ownerPath] = self::getUidAndFilename($target);
557
+
558
+            // file has been deleted in between
559
+            if (empty($ownerPath)) {
560
+                return false;
561
+            }
562
+
563
+            if ($timestamp) {
564
+                $versionedFile = $filename;
565
+            } else {
566
+                $versionedFile = $file;
567
+            }
568
+
569
+            if ($view->is_dir('/files_trashbin/versions/' . $file)) {
570
+                $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
571
+            } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
572
+                foreach ($versions as $v) {
573
+                    if ($timestamp) {
574
+                        $rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
575
+                    } else {
576
+                        $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
577
+                    }
578
+                }
579
+            }
580
+        }
581
+    }
582
+
583
+    /**
584
+     * delete all files from the trash
585
+     */
586
+    public static function deleteAll() {
587
+        $user = OC_User::getUser();
588
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
589
+        $view = new View('/' . $user);
590
+        $fileInfos = $view->getDirectoryContent('files_trashbin/files');
591
+
592
+        try {
593
+            $trash = $userRoot->get('files_trashbin');
594
+        } catch (NotFoundException $e) {
595
+            return false;
596
+        }
597
+
598
+        // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
599
+        $filePaths = [];
600
+        foreach ($fileInfos as $fileInfo) {
601
+            $filePaths[] = $view->getRelativePath($fileInfo->getPath());
602
+        }
603
+        unset($fileInfos); // save memory
604
+
605
+        // Bulk PreDelete-Hook
606
+        \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
607
+
608
+        // Single-File Hooks
609
+        foreach ($filePaths as $path) {
610
+            self::emitTrashbinPreDelete($path);
611
+        }
612
+
613
+        // actual file deletion
614
+        $trash->delete();
615
+
616
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
617
+        $query->delete('files_trash')
618
+            ->where($query->expr()->eq('user', $query->createNamedParameter($user)));
619
+        $query->executeStatement();
620
+
621
+        // Bulk PostDelete-Hook
622
+        \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
623
+
624
+        // Single-File Hooks
625
+        foreach ($filePaths as $path) {
626
+            self::emitTrashbinPostDelete($path);
627
+        }
628
+
629
+        $trash = $userRoot->newFolder('files_trashbin');
630
+        $trash->newFolder('files');
631
+
632
+        return true;
633
+    }
634
+
635
+    /**
636
+     * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
637
+     *
638
+     * @param string $path
639
+     */
640
+    protected static function emitTrashbinPreDelete($path) {
641
+        \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
642
+    }
643
+
644
+    /**
645
+     * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
646
+     *
647
+     * @param string $path
648
+     */
649
+    protected static function emitTrashbinPostDelete($path) {
650
+        \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
651
+    }
652
+
653
+    /**
654
+     * delete file from trash bin permanently
655
+     *
656
+     * @param string $filename path to the file
657
+     * @param string $user
658
+     * @param int $timestamp of deletion time
659
+     *
660
+     * @return int|float size of deleted files
661
+     */
662
+    public static function delete($filename, $user, $timestamp = null) {
663
+        $userRoot = \OC::$server->getUserFolder($user)->getParent();
664
+        $view = new View('/' . $user);
665
+        $size = 0;
666
+
667
+        if ($timestamp) {
668
+            $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
669
+            $query->delete('files_trash')
670
+                ->where($query->expr()->eq('user', $query->createNamedParameter($user)))
671
+                ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename)))
672
+                ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp)));
673
+            $query->executeStatement();
674
+
675
+            $file = static::getTrashFilename($filename, $timestamp);
676
+        } else {
677
+            $file = $filename;
678
+        }
679
+
680
+        $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
681
+
682
+        try {
683
+            $node = $userRoot->get('/files_trashbin/files/' . $file);
684
+        } catch (NotFoundException $e) {
685
+            return $size;
686
+        }
687
+
688
+        if ($node instanceof Folder) {
689
+            $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
690
+        } elseif ($node instanceof File) {
691
+            $size += $view->filesize('/files_trashbin/files/' . $file);
692
+        }
693
+
694
+        self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
695
+        $node->delete();
696
+        self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
697
+
698
+        return $size;
699
+    }
700
+
701
+    /**
702
+     * @param string $file
703
+     * @param string $filename
704
+     * @param ?int $timestamp
705
+     */
706
+    private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
707
+        $size = 0;
708
+        if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
709
+            if ($view->is_dir('files_trashbin/versions/' . $file)) {
710
+                $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
711
+                $view->unlink('files_trashbin/versions/' . $file);
712
+            } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
713
+                foreach ($versions as $v) {
714
+                    if ($timestamp) {
715
+                        $size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
716
+                        $view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
717
+                    } else {
718
+                        $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
719
+                        $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
720
+                    }
721
+                }
722
+            }
723
+        }
724
+        return $size;
725
+    }
726
+
727
+    /**
728
+     * check to see whether a file exists in trashbin
729
+     *
730
+     * @param string $filename path to the file
731
+     * @param int $timestamp of deletion time
732
+     * @return bool true if file exists, otherwise false
733
+     */
734
+    public static function file_exists($filename, $timestamp = null) {
735
+        $user = OC_User::getUser();
736
+        $view = new View('/' . $user);
737
+
738
+        if ($timestamp) {
739
+            $filename = static::getTrashFilename($filename, $timestamp);
740
+        }
741
+
742
+        $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
743
+        return $view->file_exists($target);
744
+    }
745
+
746
+    /**
747
+     * deletes used space for trash bin in db if user was deleted
748
+     *
749
+     * @param string $uid id of deleted user
750
+     * @return bool result of db delete operation
751
+     */
752
+    public static function deleteUser($uid) {
753
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
754
+        $query->delete('files_trash')
755
+            ->where($query->expr()->eq('user', $query->createNamedParameter($uid)));
756
+        return (bool) $query->executeStatement();
757
+    }
758
+
759
+    /**
760
+     * calculate remaining free space for trash bin
761
+     *
762
+     * @param int|float $trashbinSize current size of the trash bin
763
+     * @param string $user
764
+     * @return int|float available free space for trash bin
765
+     */
766
+    private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
767
+        $configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
768
+        if ($configuredTrashbinSize > -1) {
769
+            return $configuredTrashbinSize - $trashbinSize;
770
+        }
771
+
772
+        $userObject = \OC::$server->getUserManager()->get($user);
773
+        if (is_null($userObject)) {
774
+            return 0;
775
+        }
776
+        $softQuota = true;
777
+        $quota = $userObject->getQuota();
778
+        if ($quota === null || $quota === 'none') {
779
+            $quota = Filesystem::free_space('/');
780
+            $softQuota = false;
781
+            // inf or unknown free space
782
+            if ($quota < 0) {
783
+                $quota = PHP_INT_MAX;
784
+            }
785
+        } else {
786
+            $quota = \OCP\Util::computerFileSize($quota);
787
+            // invalid quota
788
+            if ($quota === false) {
789
+                $quota = PHP_INT_MAX;
790
+            }
791
+        }
792
+
793
+        // calculate available space for trash bin
794
+        // subtract size of files and current trash bin size from quota
795
+        if ($softQuota) {
796
+            $userFolder = \OC::$server->getUserFolder($user);
797
+            if (is_null($userFolder)) {
798
+                return 0;
799
+            }
800
+            $free = $quota - $userFolder->getSize(false); // remaining free space for user
801
+            if ($free > 0) {
802
+                $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
803
+            } else {
804
+                $availableSpace = $free - $trashbinSize;
805
+            }
806
+        } else {
807
+            $availableSpace = $quota;
808
+        }
809
+
810
+        return \OCP\Util::numericToNumber($availableSpace);
811
+    }
812
+
813
+    /**
814
+     * resize trash bin if necessary after a new file was added to Nextcloud
815
+     *
816
+     * @param string $user user id
817
+     */
818
+    public static function resizeTrash($user) {
819
+        $size = self::getTrashbinSize($user);
820
+
821
+        $freeSpace = self::calculateFreeSpace($size, $user);
822
+
823
+        if ($freeSpace < 0) {
824
+            self::scheduleExpire($user);
825
+        }
826
+    }
827
+
828
+    /**
829
+     * clean up the trash bin
830
+     *
831
+     * @param string $user
832
+     */
833
+    public static function expire($user) {
834
+        $trashBinSize = self::getTrashbinSize($user);
835
+        $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
836
+
837
+        $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
838
+
839
+        // delete all files older then $retention_obligation
840
+        [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
841
+
842
+        $availableSpace += $delSize;
843
+
844
+        // delete files from trash until we meet the trash bin size limit again
845
+        self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
846
+    }
847
+
848
+    /**
849
+     * @param string $user
850
+     */
851
+    private static function scheduleExpire($user) {
852
+        // let the admin disable auto expire
853
+        /** @var Application $application */
854
+        $application = \OC::$server->query(Application::class);
855
+        $expiration = $application->getContainer()->query('Expiration');
856
+        if ($expiration->isEnabled()) {
857
+            \OC::$server->getCommandBus()->push(new Expire($user));
858
+        }
859
+    }
860
+
861
+    /**
862
+     * if the size limit for the trash bin is reached, we delete the oldest
863
+     * files in the trash bin until we meet the limit again
864
+     *
865
+     * @param array $files
866
+     * @param string $user
867
+     * @param int|float $availableSpace available disc space
868
+     * @return int|float size of deleted files
869
+     */
870
+    protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
871
+        /** @var Application $application */
872
+        $application = \OC::$server->query(Application::class);
873
+        $expiration = $application->getContainer()->query('Expiration');
874
+        $size = 0;
875
+
876
+        if ($availableSpace < 0) {
877
+            foreach ($files as $file) {
878
+                if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
879
+                    $tmp = self::delete($file['name'], $user, $file['mtime']);
880
+                    \OC::$server->get(LoggerInterface::class)->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
881
+                    $availableSpace += $tmp;
882
+                    $size += $tmp;
883
+                } else {
884
+                    break;
885
+                }
886
+            }
887
+        }
888
+        return $size;
889
+    }
890
+
891
+    /**
892
+     * delete files older then max storage time
893
+     *
894
+     * @param array $files list of files sorted by mtime
895
+     * @param string $user
896
+     * @return array{int|float, int} size of deleted files and number of deleted files
897
+     */
898
+    public static function deleteExpiredFiles($files, $user) {
899
+        /** @var Expiration $expiration */
900
+        $expiration = \OC::$server->query(Expiration::class);
901
+        $size = 0;
902
+        $count = 0;
903
+        foreach ($files as $file) {
904
+            $timestamp = $file['mtime'];
905
+            $filename = $file['name'];
906
+            if ($expiration->isExpired($timestamp)) {
907
+                try {
908
+                    $size += self::delete($filename, $user, $timestamp);
909
+                    $count++;
910
+                } catch (\OCP\Files\NotPermittedException $e) {
911
+                    \OC::$server->get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed.',
912
+                        [
913
+                            'exception' => $e,
914
+                            'app' => 'files_trashbin',
915
+                        ]
916
+                    );
917
+                }
918
+                \OC::$server->get(LoggerInterface::class)->info(
919
+                    'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
920
+                    ['app' => 'files_trashbin']
921
+                );
922
+            } else {
923
+                break;
924
+            }
925
+        }
926
+
927
+        return [$size, $count];
928
+    }
929
+
930
+    /**
931
+     * recursive copy to copy a whole directory
932
+     *
933
+     * @param string $source source path, relative to the users files directory
934
+     * @param string $destination destination path relative to the users root directory
935
+     * @param View $view file view for the users root directory
936
+     * @return int|float
937
+     * @throws Exceptions\CopyRecursiveException
938
+     */
939
+    private static function copy_recursive($source, $destination, View $view): int|float {
940
+        $size = 0;
941
+        if ($view->is_dir($source)) {
942
+            $view->mkdir($destination);
943
+            $view->touch($destination, $view->filemtime($source));
944
+            foreach ($view->getDirectoryContent($source) as $i) {
945
+                $pathDir = $source . '/' . $i['name'];
946
+                if ($view->is_dir($pathDir)) {
947
+                    $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
948
+                } else {
949
+                    $size += $view->filesize($pathDir);
950
+                    $result = $view->copy($pathDir, $destination . '/' . $i['name']);
951
+                    if (!$result) {
952
+                        throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
953
+                    }
954
+                    $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
955
+                }
956
+            }
957
+        } else {
958
+            $size += $view->filesize($source);
959
+            $result = $view->copy($source, $destination);
960
+            if (!$result) {
961
+                throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
962
+            }
963
+            $view->touch($destination, $view->filemtime($source));
964
+        }
965
+        return $size;
966
+    }
967
+
968
+    /**
969
+     * find all versions which belong to the file we want to restore
970
+     *
971
+     * @param string $filename name of the file which should be restored
972
+     * @param int $timestamp timestamp when the file was deleted
973
+     */
974
+    private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
975
+        $view = new View('/' . $user . '/files_trashbin/versions');
976
+        $versions = [];
977
+
978
+        /** @var \OC\Files\Storage\Storage $storage */
979
+        [$storage,] = $view->resolvePath('/');
980
+
981
+        $pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
982
+        if ($timestamp) {
983
+            // fetch for old versions
984
+            $escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
985
+            $pattern .= '.v%.d' . $escapedTimestamp;
986
+            $offset = -strlen($escapedTimestamp) - 2;
987
+        } else {
988
+            $pattern .= '.v%';
989
+        }
990
+
991
+        // Manually fetch all versions from the file cache to be able to filter them by their parent
992
+        $cache = $storage->getCache('');
993
+        $query = new CacheQueryBuilder(
994
+            \OC::$server->getDatabaseConnection(),
995
+            \OC::$server->getSystemConfig(),
996
+            \OC::$server->get(LoggerInterface::class)
997
+        );
998
+        $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
999
+        $parentId = $cache->getId($normalizedParentPath);
1000
+        if ($parentId === -1) {
1001
+            return [];
1002
+        }
1003
+
1004
+        $query->selectFileCache()
1005
+            ->whereStorageId($cache->getNumericStorageId())
1006
+            ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId)))
1007
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
1008
+
1009
+        $result = $query->executeQuery();
1010
+        $entries = $result->fetchAll();
1011
+        $result->closeCursor();
1012
+
1013
+        /** @var CacheEntry[] $matches */
1014
+        $matches = array_map(function (array $data) {
1015
+            return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1016
+        }, $entries);
1017
+
1018
+        foreach ($matches as $ma) {
1019
+            if ($timestamp) {
1020
+                $parts = explode('.v', substr($ma['path'], 0, $offset));
1021
+                $versions[] = end($parts);
1022
+            } else {
1023
+                $parts = explode('.v', $ma['path']);
1024
+                $versions[] = end($parts);
1025
+            }
1026
+        }
1027
+
1028
+        return $versions;
1029
+    }
1030
+
1031
+    /**
1032
+     * find unique extension for restored file if a file with the same name already exists
1033
+     *
1034
+     * @param string $location where the file should be restored
1035
+     * @param string $filename name of the file
1036
+     * @param View $view filesystem view relative to users root directory
1037
+     * @return string with unique extension
1038
+     */
1039
+    private static function getUniqueFilename($location, $filename, View $view) {
1040
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
1041
+        $name = pathinfo($filename, PATHINFO_FILENAME);
1042
+        $l = \OC::$server->getL10N('files_trashbin');
1043
+
1044
+        $location = '/' . trim($location, '/');
1045
+
1046
+        // if extension is not empty we set a dot in front of it
1047
+        if ($ext !== '') {
1048
+            $ext = '.' . $ext;
1049
+        }
1050
+
1051
+        if ($view->file_exists('files' . $location . '/' . $filename)) {
1052
+            $i = 2;
1053
+            $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1054
+            while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1055
+                $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1056
+                $i++;
1057
+            }
1058
+
1059
+            return $uniqueName;
1060
+        }
1061
+
1062
+        return $filename;
1063
+    }
1064
+
1065
+    /**
1066
+     * get the size from a given root folder
1067
+     *
1068
+     * @param View $view file view on the root folder
1069
+     * @return int|float size of the folder
1070
+     */
1071
+    private static function calculateSize(View $view): int|float {
1072
+        $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1073
+        if (!file_exists($root)) {
1074
+            return 0;
1075
+        }
1076
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
1077
+        $size = 0;
1078
+
1079
+        /**
1080
+         * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
1081
+         * This bug is fixed in PHP 5.5.9 or before
1082
+         * See #8376
1083
+         */
1084
+        $iterator->rewind();
1085
+        while ($iterator->valid()) {
1086
+            $path = $iterator->current();
1087
+            $relpath = substr($path, strlen($root) - 1);
1088
+            if (!$view->is_dir($relpath)) {
1089
+                $size += $view->filesize($relpath);
1090
+            }
1091
+            $iterator->next();
1092
+        }
1093
+        return $size;
1094
+    }
1095
+
1096
+    /**
1097
+     * get current size of trash bin from a given user
1098
+     *
1099
+     * @param string $user user who owns the trash bin
1100
+     * @return int|float trash bin size
1101
+     */
1102
+    private static function getTrashbinSize(string $user): int|float {
1103
+        $view = new View('/' . $user);
1104
+        $fileInfo = $view->getFileInfo('/files_trashbin');
1105
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1106
+    }
1107
+
1108
+    /**
1109
+     * check if trash bin is empty for a given user
1110
+     *
1111
+     * @param string $user
1112
+     * @return bool
1113
+     */
1114
+    public static function isEmpty($user) {
1115
+        $view = new View('/' . $user . '/files_trashbin');
1116
+        if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1117
+            while ($file = readdir($dh)) {
1118
+                if (!Filesystem::isIgnoredDir($file)) {
1119
+                    return false;
1120
+                }
1121
+            }
1122
+        }
1123
+        return true;
1124
+    }
1125
+
1126
+    /**
1127
+     * @param $path
1128
+     * @return string
1129
+     */
1130
+    public static function preview_icon($path) {
1131
+        return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
1132
+    }
1133
+
1134
+    /**
1135
+     * Return the filename used in the trash bin
1136
+     */
1137
+    public static function getTrashFilename(string $filename, int $timestamp): string {
1138
+        $trashFilename = $filename . '.d' . $timestamp;
1139
+        $length = strlen($trashFilename);
1140
+        // oc_filecache `name` column has a limit of 250 chars
1141
+        $maxLength = 250;
1142
+        if ($length > $maxLength) {
1143
+            $trashFilename = substr_replace(
1144
+                $trashFilename,
1145
+                '',
1146
+                $maxLength / 2,
1147
+                $length - $maxLength
1148
+            );
1149
+        }
1150
+        return $trashFilename;
1151
+    }
1152 1152
 }
Please login to merge, or discard this patch.
Spacing   +73 added lines, -73 removed lines patch added patch discarded remove patch
@@ -106,7 +106,7 @@  discard block
 block discarded – undo
106 106
 		Filesystem::initMountPoints($uid);
107 107
 		if ($uid !== OC_User::getUser()) {
108 108
 			$info = Filesystem::getFileInfo($filename);
109
-			$ownerView = new View('/' . $uid . '/files');
109
+			$ownerView = new View('/'.$uid.'/files');
110 110
 			try {
111 111
 				$filename = $ownerView->getPath($info['fileid']);
112 112
 			} catch (NotFoundException $e) {
@@ -168,7 +168,7 @@  discard block
 block discarded – undo
168 168
 	}
169 169
 
170 170
 	private static function setUpTrash($user) {
171
-		$view = new View('/' . $user);
171
+		$view = new View('/'.$user);
172 172
 		if (!$view->is_dir('files_trashbin')) {
173 173
 			$view->mkdir('files_trashbin');
174 174
 		}
@@ -203,8 +203,8 @@  discard block
 block discarded – undo
203 203
 
204 204
 		$view = new View('/');
205 205
 
206
-		$target = $user . '/files_trashbin/files/' . static::getTrashFilename($targetFilename, $timestamp);
207
-		$source = $owner . '/files_trashbin/files/' . static::getTrashFilename($sourceFilename, $timestamp);
206
+		$target = $user.'/files_trashbin/files/'.static::getTrashFilename($targetFilename, $timestamp);
207
+		$source = $owner.'/files_trashbin/files/'.static::getTrashFilename($sourceFilename, $timestamp);
208 208
 		$free = $view->free_space($target);
209 209
 		$isUnknownOrUnlimitedFreeSpace = $free < 0;
210 210
 		$isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
@@ -248,14 +248,14 @@  discard block
 block discarded – undo
248 248
 			$ownerPath = $file_path;
249 249
 		}
250 250
 
251
-		$ownerView = new View('/' . $owner);
251
+		$ownerView = new View('/'.$owner);
252 252
 
253 253
 		// file has been deleted in between
254 254
 		if (is_null($ownerPath) || $ownerPath === '') {
255 255
 			return true;
256 256
 		}
257 257
 
258
-		$sourceInfo = $ownerView->getFileInfo('/files/' . $ownerPath);
258
+		$sourceInfo = $ownerView->getFileInfo('/files/'.$ownerPath);
259 259
 
260 260
 		if ($sourceInfo === false) {
261 261
 			return true;
@@ -278,7 +278,7 @@  discard block
 block discarded – undo
278 278
 		$lockingProvider = \OC::$server->getLockingProvider();
279 279
 
280 280
 		// disable proxy to prevent recursive calls
281
-		$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
281
+		$trashPath = '/files_trashbin/files/'.static::getTrashFilename($filename, $timestamp);
282 282
 		$gotLock = false;
283 283
 
284 284
 		while (!$gotLock) {
@@ -294,7 +294,7 @@  discard block
 block discarded – undo
294 294
 
295 295
 				$timestamp = $timestamp + 1;
296 296
 
297
-				$trashPath = '/files_trashbin/files/' . static::getTrashFilename($filename, $timestamp);
297
+				$trashPath = '/files_trashbin/files/'.static::getTrashFilename($filename, $timestamp);
298 298
 			}
299 299
 		}
300 300
 
@@ -324,7 +324,7 @@  discard block
 block discarded – undo
324 324
 			if ($trashStorage->file_exists($trashInternalPath)) {
325 325
 				$trashStorage->unlink($trashInternalPath);
326 326
 			}
327
-			\OC::$server->get(LoggerInterface::class)->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
327
+			\OC::$server->get(LoggerInterface::class)->error('Couldn\'t move '.$file_path.' to the trash bin', ['app' => 'files_trashbin']);
328 328
 		}
329 329
 
330 330
 		if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
@@ -377,7 +377,7 @@  discard block
 block discarded – undo
377 377
 		return $moveSuccessful;
378 378
 	}
379 379
 
380
-	private static function getConfiguredTrashbinSize(string $user): int|float {
380
+	private static function getConfiguredTrashbinSize(string $user): int | float {
381 381
 		$config = \OC::$server->get(IConfig::class);
382 382
 		$userTrashbinSize = $config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
383 383
 		if (is_numeric($userTrashbinSize) && ($userTrashbinSize > -1)) {
@@ -403,17 +403,17 @@  discard block
 block discarded – undo
403 403
 			$user = OC_User::getUser();
404 404
 			$rootView = new View('/');
405 405
 
406
-			if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
406
+			if ($rootView->is_dir($owner.'/files_versions/'.$ownerPath)) {
407 407
 				if ($owner !== $user) {
408
-					self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
408
+					self::copy_recursive($owner.'/files_versions/'.$ownerPath, $owner.'/files_trashbin/versions/'.static::getTrashFilename(basename($ownerPath), $timestamp), $rootView);
409 409
 				}
410
-				self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . static::getTrashFilename($filename, $timestamp));
410
+				self::move($rootView, $owner.'/files_versions/'.$ownerPath, $user.'/files_trashbin/versions/'.static::getTrashFilename($filename, $timestamp));
411 411
 			} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
412 412
 				foreach ($versions as $v) {
413 413
 					if ($owner !== $user) {
414
-						self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . static::getTrashFilename($v['name'] . '.v' . $v['version'], $timestamp));
414
+						self::copy($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $owner.'/files_trashbin/versions/'.static::getTrashFilename($v['name'].'.v'.$v['version'], $timestamp));
415 415
 					}
416
-					self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v['version'], $timestamp));
416
+					self::move($rootView, $owner.'/files_versions'.$v['path'].'.v'.$v['version'], $user.'/files_trashbin/versions/'.static::getTrashFilename($filename.'.v'.$v['version'], $timestamp));
417 417
 				}
418 418
 			}
419 419
 		}
@@ -475,18 +475,18 @@  discard block
 block discarded – undo
475 475
 	 */
476 476
 	public static function restore($file, $filename, $timestamp) {
477 477
 		$user = OC_User::getUser();
478
-		$view = new View('/' . $user);
478
+		$view = new View('/'.$user);
479 479
 
480 480
 		$location = '';
481 481
 		if ($timestamp) {
482 482
 			$location = self::getLocation($user, $filename, $timestamp);
483 483
 			if ($location === false) {
484
-				\OC::$server->get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
484
+				\OC::$server->get(LoggerInterface::class)->error('trash bin database inconsistent! ($user: '.$user.' $filename: '.$filename.', $timestamp: '.$timestamp.')', ['app' => 'files_trashbin']);
485 485
 			} else {
486 486
 				// if location no longer exists, restore file in the root directory
487 487
 				if ($location !== '/' &&
488
-					(!$view->is_dir('files/' . $location) ||
489
-						!$view->isCreatable('files/' . $location))
488
+					(!$view->is_dir('files/'.$location) ||
489
+						!$view->isCreatable('files/'.$location))
490 490
 				) {
491 491
 					$location = '';
492 492
 				}
@@ -496,8 +496,8 @@  discard block
 block discarded – undo
496 496
 		// we need a  extension in case a file/dir with the same name already exists
497 497
 		$uniqueFilename = self::getUniqueFilename($location, $filename, $view);
498 498
 
499
-		$source = Filesystem::normalizePath('files_trashbin/files/' . $file);
500
-		$target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
499
+		$source = Filesystem::normalizePath('files_trashbin/files/'.$file);
500
+		$target = Filesystem::normalizePath('files/'.$location.'/'.$uniqueFilename);
501 501
 		if (!$view->file_exists($source)) {
502 502
 			return false;
503 503
 		}
@@ -512,10 +512,10 @@  discard block
 block discarded – undo
512 512
 		// handle the restore result
513 513
 		if ($restoreResult) {
514 514
 			$fakeRoot = $view->getRoot();
515
-			$view->chroot('/' . $user . '/files');
516
-			$view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
515
+			$view->chroot('/'.$user.'/files');
516
+			$view->touch('/'.$location.'/'.$uniqueFilename, $mtime);
517 517
 			$view->chroot($fakeRoot);
518
-			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
518
+			\OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename),
519 519
 				'trashPath' => Filesystem::normalizePath($file)]);
520 520
 
521 521
 			self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
@@ -551,7 +551,7 @@  discard block
 block discarded – undo
551 551
 			$user = OC_User::getUser();
552 552
 			$rootView = new View('/');
553 553
 
554
-			$target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
554
+			$target = Filesystem::normalizePath('/'.$location.'/'.$uniqueFilename);
555 555
 
556 556
 			[$owner, $ownerPath] = self::getUidAndFilename($target);
557 557
 
@@ -566,14 +566,14 @@  discard block
 block discarded – undo
566 566
 				$versionedFile = $file;
567 567
 			}
568 568
 
569
-			if ($view->is_dir('/files_trashbin/versions/' . $file)) {
570
-				$rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
569
+			if ($view->is_dir('/files_trashbin/versions/'.$file)) {
570
+				$rootView->rename(Filesystem::normalizePath($user.'/files_trashbin/versions/'.$file), Filesystem::normalizePath($owner.'/files_versions/'.$ownerPath));
571 571
 			} elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
572 572
 				foreach ($versions as $v) {
573 573
 					if ($timestamp) {
574
-						$rootView->rename($user . '/files_trashbin/versions/' . static::getTrashFilename($versionedFile . '.v' . $v, $timestamp), $owner . '/files_versions/' . $ownerPath . '.v' . $v);
574
+						$rootView->rename($user.'/files_trashbin/versions/'.static::getTrashFilename($versionedFile.'.v'.$v, $timestamp), $owner.'/files_versions/'.$ownerPath.'.v'.$v);
575 575
 					} else {
576
-						$rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
576
+						$rootView->rename($user.'/files_trashbin/versions/'.$versionedFile.'.v'.$v, $owner.'/files_versions/'.$ownerPath.'.v'.$v);
577 577
 					}
578 578
 				}
579 579
 			}
@@ -586,7 +586,7 @@  discard block
 block discarded – undo
586 586
 	public static function deleteAll() {
587 587
 		$user = OC_User::getUser();
588 588
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
589
-		$view = new View('/' . $user);
589
+		$view = new View('/'.$user);
590 590
 		$fileInfos = $view->getDirectoryContent('files_trashbin/files');
591 591
 
592 592
 		try {
@@ -661,7 +661,7 @@  discard block
 block discarded – undo
661 661
 	 */
662 662
 	public static function delete($filename, $user, $timestamp = null) {
663 663
 		$userRoot = \OC::$server->getUserFolder($user)->getParent();
664
-		$view = new View('/' . $user);
664
+		$view = new View('/'.$user);
665 665
 		$size = 0;
666 666
 
667 667
 		if ($timestamp) {
@@ -680,20 +680,20 @@  discard block
 block discarded – undo
680 680
 		$size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
681 681
 
682 682
 		try {
683
-			$node = $userRoot->get('/files_trashbin/files/' . $file);
683
+			$node = $userRoot->get('/files_trashbin/files/'.$file);
684 684
 		} catch (NotFoundException $e) {
685 685
 			return $size;
686 686
 		}
687 687
 
688 688
 		if ($node instanceof Folder) {
689
-			$size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
689
+			$size += self::calculateSize(new View('/'.$user.'/files_trashbin/files/'.$file));
690 690
 		} elseif ($node instanceof File) {
691
-			$size += $view->filesize('/files_trashbin/files/' . $file);
691
+			$size += $view->filesize('/files_trashbin/files/'.$file);
692 692
 		}
693 693
 
694
-		self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
694
+		self::emitTrashbinPreDelete('/files_trashbin/files/'.$file);
695 695
 		$node->delete();
696
-		self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
696
+		self::emitTrashbinPostDelete('/files_trashbin/files/'.$file);
697 697
 
698 698
 		return $size;
699 699
 	}
@@ -703,20 +703,20 @@  discard block
 block discarded – undo
703 703
 	 * @param string $filename
704 704
 	 * @param ?int $timestamp
705 705
 	 */
706
-	private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int|float {
706
+	private static function deleteVersions(View $view, $file, $filename, $timestamp, string $user): int | float {
707 707
 		$size = 0;
708 708
 		if (\OCP\Server::get(IAppManager::class)->isEnabledForUser('files_versions')) {
709
-			if ($view->is_dir('files_trashbin/versions/' . $file)) {
710
-				$size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
711
-				$view->unlink('files_trashbin/versions/' . $file);
709
+			if ($view->is_dir('files_trashbin/versions/'.$file)) {
710
+				$size += self::calculateSize(new View('/'.$user.'/files_trashbin/versions/'.$file));
711
+				$view->unlink('files_trashbin/versions/'.$file);
712 712
 			} elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
713 713
 				foreach ($versions as $v) {
714 714
 					if ($timestamp) {
715
-						$size += $view->filesize('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
716
-						$view->unlink('/files_trashbin/versions/' . static::getTrashFilename($filename . '.v' . $v, $timestamp));
715
+						$size += $view->filesize('/files_trashbin/versions/'.static::getTrashFilename($filename.'.v'.$v, $timestamp));
716
+						$view->unlink('/files_trashbin/versions/'.static::getTrashFilename($filename.'.v'.$v, $timestamp));
717 717
 					} else {
718
-						$size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
719
-						$view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
718
+						$size += $view->filesize('/files_trashbin/versions/'.$filename.'.v'.$v);
719
+						$view->unlink('/files_trashbin/versions/'.$filename.'.v'.$v);
720 720
 					}
721 721
 				}
722 722
 			}
@@ -733,13 +733,13 @@  discard block
 block discarded – undo
733 733
 	 */
734 734
 	public static function file_exists($filename, $timestamp = null) {
735 735
 		$user = OC_User::getUser();
736
-		$view = new View('/' . $user);
736
+		$view = new View('/'.$user);
737 737
 
738 738
 		if ($timestamp) {
739 739
 			$filename = static::getTrashFilename($filename, $timestamp);
740 740
 		}
741 741
 
742
-		$target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
742
+		$target = Filesystem::normalizePath('files_trashbin/files/'.$filename);
743 743
 		return $view->file_exists($target);
744 744
 	}
745 745
 
@@ -763,7 +763,7 @@  discard block
 block discarded – undo
763 763
 	 * @param string $user
764 764
 	 * @return int|float available free space for trash bin
765 765
 	 */
766
-	private static function calculateFreeSpace(int|float $trashbinSize, string $user): int|float {
766
+	private static function calculateFreeSpace(int | float $trashbinSize, string $user): int | float {
767 767
 		$configuredTrashbinSize = static::getConfiguredTrashbinSize($user);
768 768
 		if ($configuredTrashbinSize > -1) {
769 769
 			return $configuredTrashbinSize - $trashbinSize;
@@ -867,7 +867,7 @@  discard block
 block discarded – undo
867 867
 	 * @param int|float $availableSpace available disc space
868 868
 	 * @return int|float size of deleted files
869 869
 	 */
870
-	protected static function deleteFiles(array $files, string $user, int|float $availableSpace): int|float {
870
+	protected static function deleteFiles(array $files, string $user, int | float $availableSpace): int | float {
871 871
 		/** @var Application $application */
872 872
 		$application = \OC::$server->query(Application::class);
873 873
 		$expiration = $application->getContainer()->query('Expiration');
@@ -877,7 +877,7 @@  discard block
 block discarded – undo
877 877
 			foreach ($files as $file) {
878 878
 				if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
879 879
 					$tmp = self::delete($file['name'], $user, $file['mtime']);
880
-					\OC::$server->get(LoggerInterface::class)->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
880
+					\OC::$server->get(LoggerInterface::class)->info('remove "'.$file['name'].'" ('.$tmp.'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
881 881
 					$availableSpace += $tmp;
882 882
 					$size += $tmp;
883 883
 				} else {
@@ -908,7 +908,7 @@  discard block
 block discarded – undo
908 908
 					$size += self::delete($filename, $user, $timestamp);
909 909
 					$count++;
910 910
 				} catch (\OCP\Files\NotPermittedException $e) {
911
-					\OC::$server->get(LoggerInterface::class)->warning('Removing "' . $filename . '" from trashbin failed.',
911
+					\OC::$server->get(LoggerInterface::class)->warning('Removing "'.$filename.'" from trashbin failed.',
912 912
 						[
913 913
 							'exception' => $e,
914 914
 							'app' => 'files_trashbin',
@@ -916,7 +916,7 @@  discard block
 block discarded – undo
916 916
 					);
917 917
 				}
918 918
 				\OC::$server->get(LoggerInterface::class)->info(
919
-					'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
919
+					'Remove "'.$filename.'" from trashbin because it exceeds max retention obligation term.',
920 920
 					['app' => 'files_trashbin']
921 921
 				);
922 922
 			} else {
@@ -936,22 +936,22 @@  discard block
 block discarded – undo
936 936
 	 * @return int|float
937 937
 	 * @throws Exceptions\CopyRecursiveException
938 938
 	 */
939
-	private static function copy_recursive($source, $destination, View $view): int|float {
939
+	private static function copy_recursive($source, $destination, View $view): int | float {
940 940
 		$size = 0;
941 941
 		if ($view->is_dir($source)) {
942 942
 			$view->mkdir($destination);
943 943
 			$view->touch($destination, $view->filemtime($source));
944 944
 			foreach ($view->getDirectoryContent($source) as $i) {
945
-				$pathDir = $source . '/' . $i['name'];
945
+				$pathDir = $source.'/'.$i['name'];
946 946
 				if ($view->is_dir($pathDir)) {
947
-					$size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
947
+					$size += self::copy_recursive($pathDir, $destination.'/'.$i['name'], $view);
948 948
 				} else {
949 949
 					$size += $view->filesize($pathDir);
950
-					$result = $view->copy($pathDir, $destination . '/' . $i['name']);
950
+					$result = $view->copy($pathDir, $destination.'/'.$i['name']);
951 951
 					if (!$result) {
952 952
 						throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
953 953
 					}
954
-					$view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
954
+					$view->touch($destination.'/'.$i['name'], $view->filemtime($pathDir));
955 955
 				}
956 956
 			}
957 957
 		} else {
@@ -972,17 +972,17 @@  discard block
 block discarded – undo
972 972
 	 * @param int $timestamp timestamp when the file was deleted
973 973
 	 */
974 974
 	private static function getVersionsFromTrash($filename, $timestamp, string $user): array {
975
-		$view = new View('/' . $user . '/files_trashbin/versions');
975
+		$view = new View('/'.$user.'/files_trashbin/versions');
976 976
 		$versions = [];
977 977
 
978 978
 		/** @var \OC\Files\Storage\Storage $storage */
979
-		[$storage,] = $view->resolvePath('/');
979
+		[$storage, ] = $view->resolvePath('/');
980 980
 
981 981
 		$pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename));
982 982
 		if ($timestamp) {
983 983
 			// fetch for old versions
984 984
 			$escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp);
985
-			$pattern .= '.v%.d' . $escapedTimestamp;
985
+			$pattern .= '.v%.d'.$escapedTimestamp;
986 986
 			$offset = -strlen($escapedTimestamp) - 2;
987 987
 		} else {
988 988
 			$pattern .= '.v%';
@@ -995,7 +995,7 @@  discard block
 block discarded – undo
995 995
 			\OC::$server->getSystemConfig(),
996 996
 			\OC::$server->get(LoggerInterface::class)
997 997
 		);
998
-		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/');
998
+		$normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'.$filename)), '/');
999 999
 		$parentId = $cache->getId($normalizedParentPath);
1000 1000
 		if ($parentId === -1) {
1001 1001
 			return [];
@@ -1011,7 +1011,7 @@  discard block
 block discarded – undo
1011 1011
 		$result->closeCursor();
1012 1012
 
1013 1013
 		/** @var CacheEntry[] $matches */
1014
-		$matches = array_map(function (array $data) {
1014
+		$matches = array_map(function(array $data) {
1015 1015
 			return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader());
1016 1016
 		}, $entries);
1017 1017
 
@@ -1041,18 +1041,18 @@  discard block
 block discarded – undo
1041 1041
 		$name = pathinfo($filename, PATHINFO_FILENAME);
1042 1042
 		$l = \OC::$server->getL10N('files_trashbin');
1043 1043
 
1044
-		$location = '/' . trim($location, '/');
1044
+		$location = '/'.trim($location, '/');
1045 1045
 
1046 1046
 		// if extension is not empty we set a dot in front of it
1047 1047
 		if ($ext !== '') {
1048
-			$ext = '.' . $ext;
1048
+			$ext = '.'.$ext;
1049 1049
 		}
1050 1050
 
1051
-		if ($view->file_exists('files' . $location . '/' . $filename)) {
1051
+		if ($view->file_exists('files'.$location.'/'.$filename)) {
1052 1052
 			$i = 2;
1053
-			$uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
1054
-			while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
1055
-				$uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
1053
+			$uniqueName = $name." (".$l->t("restored").")".$ext;
1054
+			while ($view->file_exists('files'.$location.'/'.$uniqueName)) {
1055
+				$uniqueName = $name." (".$l->t("restored")." ".$i.")".$ext;
1056 1056
 				$i++;
1057 1057
 			}
1058 1058
 
@@ -1068,8 +1068,8 @@  discard block
 block discarded – undo
1068 1068
 	 * @param View $view file view on the root folder
1069 1069
 	 * @return int|float size of the folder
1070 1070
 	 */
1071
-	private static function calculateSize(View $view): int|float {
1072
-		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
1071
+	private static function calculateSize(View $view): int | float {
1072
+		$root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT.'/data').$view->getAbsolutePath('');
1073 1073
 		if (!file_exists($root)) {
1074 1074
 			return 0;
1075 1075
 		}
@@ -1099,8 +1099,8 @@  discard block
 block discarded – undo
1099 1099
 	 * @param string $user user who owns the trash bin
1100 1100
 	 * @return int|float trash bin size
1101 1101
 	 */
1102
-	private static function getTrashbinSize(string $user): int|float {
1103
-		$view = new View('/' . $user);
1102
+	private static function getTrashbinSize(string $user): int | float {
1103
+		$view = new View('/'.$user);
1104 1104
 		$fileInfo = $view->getFileInfo('/files_trashbin');
1105 1105
 		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
1106 1106
 	}
@@ -1112,7 +1112,7 @@  discard block
 block discarded – undo
1112 1112
 	 * @return bool
1113 1113
 	 */
1114 1114
 	public static function isEmpty($user) {
1115
-		$view = new View('/' . $user . '/files_trashbin');
1115
+		$view = new View('/'.$user.'/files_trashbin');
1116 1116
 		if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
1117 1117
 			while ($file = readdir($dh)) {
1118 1118
 				if (!Filesystem::isIgnoredDir($file)) {
@@ -1135,7 +1135,7 @@  discard block
 block discarded – undo
1135 1135
 	 * Return the filename used in the trash bin
1136 1136
 	 */
1137 1137
 	public static function getTrashFilename(string $filename, int $timestamp): string {
1138
-		$trashFilename = $filename . '.d' . $timestamp;
1138
+		$trashFilename = $filename.'.d'.$timestamp;
1139 1139
 		$length = strlen($trashFilename);
1140 1140
 		// oc_filecache `name` column has a limit of 250 chars
1141 1141
 		$maxLength = 250;
Please login to merge, or discard this patch.