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