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