Passed
Push — master ( 140c25...2e1a56 )
by Julius
16:20 queued 15s
created
apps/files_versions/lib/Storage.php 1 patch
Indentation   +888 added lines, -888 removed lines patch added patch discarded remove patch
@@ -71,892 +71,892 @@
 block discarded – undo
71 71
 use Psr\Log\LoggerInterface;
72 72
 
73 73
 class Storage {
74
-	public const DEFAULTENABLED = true;
75
-	public const DEFAULTMAXSIZE = 50; // unit: percentage; 50% of available disk space/quota
76
-	public const VERSIONS_ROOT = 'files_versions/';
77
-
78
-	public const DELETE_TRIGGER_MASTER_REMOVED = 0;
79
-	public const DELETE_TRIGGER_RETENTION_CONSTRAINT = 1;
80
-	public const DELETE_TRIGGER_QUOTA_EXCEEDED = 2;
81
-
82
-	// files for which we can remove the versions after the delete operation was successful
83
-	private static $deletedFiles = [];
84
-
85
-	private static $sourcePathAndUser = [];
86
-
87
-	private static $max_versions_per_interval = [
88
-		//first 10sec, one version every 2sec
89
-		1 => ['intervalEndsAfter' => 10,      'step' => 2],
90
-		//next minute, one version every 10sec
91
-		2 => ['intervalEndsAfter' => 60,      'step' => 10],
92
-		//next hour, one version every minute
93
-		3 => ['intervalEndsAfter' => 3600,    'step' => 60],
94
-		//next 24h, one version every hour
95
-		4 => ['intervalEndsAfter' => 86400,   'step' => 3600],
96
-		//next 30days, one version per day
97
-		5 => ['intervalEndsAfter' => 2592000, 'step' => 86400],
98
-		//until the end one version per week
99
-		6 => ['intervalEndsAfter' => -1,      'step' => 604800],
100
-	];
101
-
102
-	/** @var \OCA\Files_Versions\AppInfo\Application */
103
-	private static $application;
104
-
105
-	/**
106
-	 * get the UID of the owner of the file and the path to the file relative to
107
-	 * owners files folder
108
-	 *
109
-	 * @param string $filename
110
-	 * @return array
111
-	 * @throws \OC\User\NoUserException
112
-	 */
113
-	public static function getUidAndFilename($filename) {
114
-		$uid = Filesystem::getOwner($filename);
115
-		$userManager = \OC::$server->get(IUserManager::class);
116
-		// if the user with the UID doesn't exists, e.g. because the UID points
117
-		// to a remote user with a federated cloud ID we use the current logged-in
118
-		// user. We need a valid local user to create the versions
119
-		if (!$userManager->userExists($uid)) {
120
-			$uid = OC_User::getUser();
121
-		}
122
-		Filesystem::initMountPoints($uid);
123
-		if ($uid !== OC_User::getUser()) {
124
-			$info = Filesystem::getFileInfo($filename);
125
-			$ownerView = new View('/'.$uid.'/files');
126
-			try {
127
-				$filename = $ownerView->getPath($info['fileid']);
128
-				// make sure that the file name doesn't end with a trailing slash
129
-				// can for example happen single files shared across servers
130
-				$filename = rtrim($filename, '/');
131
-			} catch (NotFoundException $e) {
132
-				$filename = null;
133
-			}
134
-		}
135
-		return [$uid, $filename];
136
-	}
137
-
138
-	/**
139
-	 * Remember the owner and the owner path of the source file
140
-	 *
141
-	 * @param string $source source path
142
-	 */
143
-	public static function setSourcePathAndUser($source) {
144
-		[$uid, $path] = self::getUidAndFilename($source);
145
-		self::$sourcePathAndUser[$source] = ['uid' => $uid, 'path' => $path];
146
-	}
147
-
148
-	/**
149
-	 * Gets the owner and the owner path from the source path
150
-	 *
151
-	 * @param string $source source path
152
-	 * @return array with user id and path
153
-	 */
154
-	public static function getSourcePathAndUser($source) {
155
-		if (isset(self::$sourcePathAndUser[$source])) {
156
-			$uid = self::$sourcePathAndUser[$source]['uid'];
157
-			$path = self::$sourcePathAndUser[$source]['path'];
158
-			unset(self::$sourcePathAndUser[$source]);
159
-		} else {
160
-			$uid = $path = false;
161
-		}
162
-		return [$uid, $path];
163
-	}
164
-
165
-	/**
166
-	 * get current size of all versions from a given user
167
-	 *
168
-	 * @param string $user user who owns the versions
169
-	 * @return int versions size
170
-	 */
171
-	private static function getVersionsSize($user) {
172
-		$view = new View('/' . $user);
173
-		$fileInfo = $view->getFileInfo('/files_versions');
174
-		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
175
-	}
176
-
177
-	/**
178
-	 * store a new version of a file.
179
-	 */
180
-	public static function store($filename) {
181
-		// if the file gets streamed we need to remove the .part extension
182
-		// to get the right target
183
-		$ext = pathinfo($filename, PATHINFO_EXTENSION);
184
-		if ($ext === 'part') {
185
-			$filename = substr($filename, 0, -5);
186
-		}
187
-
188
-		// we only handle existing files
189
-		if (! Filesystem::file_exists($filename) || Filesystem::is_dir($filename)) {
190
-			return false;
191
-		}
192
-
193
-		// since hook paths are always relative to the "default filesystem view"
194
-		// we always use the owner from there to get the full node
195
-		$uid = Filesystem::getView()->getOwner('');
196
-
197
-		/** @var IRootFolder $rootFolder */
198
-		$rootFolder = \OC::$server->get(IRootFolder::class);
199
-		$userFolder = $rootFolder->getUserFolder($uid);
200
-
201
-		$eventDispatcher = \OC::$server->get(IEventDispatcher::class);
202
-		try {
203
-			$file = $userFolder->get($filename);
204
-		} catch (NotFoundException $e) {
205
-			return false;
206
-		}
207
-
208
-		$mount = $file->getMountPoint();
209
-		if ($mount instanceof SharedMount) {
210
-			$ownerFolder = $rootFolder->getUserFolder($mount->getShare()->getShareOwner());
211
-			$ownerNodes = $ownerFolder->getById($file->getId());
212
-			if (count($ownerNodes)) {
213
-				$file = current($ownerNodes);
214
-				$uid = $mount->getShare()->getShareOwner();
215
-			}
216
-		}
217
-
218
-		/** @var IUserManager $userManager */
219
-		$userManager = \OC::$server->get(IUserManager::class);
220
-		$user = $userManager->get($uid);
221
-
222
-		if (!$user) {
223
-			return false;
224
-		}
225
-
226
-		// no use making versions for empty files
227
-		if ($file->getSize() === 0) {
228
-			return false;
229
-		}
230
-
231
-		$event = new CreateVersionEvent($file);
232
-		$eventDispatcher->dispatch('OCA\Files_Versions::createVersion', $event);
233
-		if ($event->shouldCreateVersion() === false) {
234
-			return false;
235
-		}
236
-
237
-		/** @var IVersionManager $versionManager */
238
-		$versionManager = \OC::$server->get(IVersionManager::class);
239
-
240
-		$versionManager->createVersion($user, $file);
241
-	}
242
-
243
-
244
-	/**
245
-	 * mark file as deleted so that we can remove the versions if the file is gone
246
-	 * @param string $path
247
-	 */
248
-	public static function markDeletedFile($path) {
249
-		[$uid, $filename] = self::getUidAndFilename($path);
250
-		self::$deletedFiles[$path] = [
251
-			'uid' => $uid,
252
-			'filename' => $filename];
253
-	}
254
-
255
-	/**
256
-	 * delete the version from the storage and cache
257
-	 *
258
-	 * @param View $view
259
-	 * @param string $path
260
-	 */
261
-	protected static function deleteVersion($view, $path) {
262
-		$view->unlink($path);
263
-		/**
264
-		 * @var \OC\Files\Storage\Storage $storage
265
-		 * @var string $internalPath
266
-		 */
267
-		[$storage, $internalPath] = $view->resolvePath($path);
268
-		$cache = $storage->getCache($internalPath);
269
-		$cache->remove($internalPath);
270
-	}
271
-
272
-	/**
273
-	 * Delete versions of a file
274
-	 */
275
-	public static function delete($path) {
276
-		$deletedFile = self::$deletedFiles[$path];
277
-		$uid = $deletedFile['uid'];
278
-		$filename = $deletedFile['filename'];
279
-
280
-		if (!Filesystem::file_exists($path)) {
281
-			$view = new View('/' . $uid . '/files_versions');
282
-
283
-			$versions = self::getVersions($uid, $filename);
284
-			if (!empty($versions)) {
285
-				foreach ($versions as $v) {
286
-					\OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
287
-					self::deleteVersion($view, $filename . '.v' . $v['version']);
288
-					\OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
289
-				}
290
-			}
291
-		}
292
-		unset(self::$deletedFiles[$path]);
293
-	}
294
-
295
-	/**
296
-	 * Delete a version of a file
297
-	 */
298
-	public static function deleteRevision(string $path, int $revision): void {
299
-		[$uid, $filename] = self::getUidAndFilename($path);
300
-		$view = new View('/' . $uid . '/files_versions');
301
-		\OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
302
-		self::deleteVersion($view, $filename . '.v' . $revision);
303
-		\OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
304
-	}
305
-
306
-	/**
307
-	 * Rename or copy versions of a file of the given paths
308
-	 *
309
-	 * @param string $sourcePath source path of the file to move, relative to
310
-	 * the currently logged in user's "files" folder
311
-	 * @param string $targetPath target path of the file to move, relative to
312
-	 * the currently logged in user's "files" folder
313
-	 * @param string $operation can be 'copy' or 'rename'
314
-	 */
315
-	public static function renameOrCopy($sourcePath, $targetPath, $operation) {
316
-		[$sourceOwner, $sourcePath] = self::getSourcePathAndUser($sourcePath);
317
-
318
-		// it was a upload of a existing file if no old path exists
319
-		// in this case the pre-hook already called the store method and we can
320
-		// stop here
321
-		if ($sourcePath === false) {
322
-			return true;
323
-		}
324
-
325
-		[$targetOwner, $targetPath] = self::getUidAndFilename($targetPath);
326
-
327
-		$sourcePath = ltrim($sourcePath, '/');
328
-		$targetPath = ltrim($targetPath, '/');
329
-
330
-		$rootView = new View('');
331
-
332
-		// did we move a directory ?
333
-		if ($rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
334
-			// does the directory exists for versions too ?
335
-			if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) {
336
-				// create missing dirs if necessary
337
-				self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
338
-
339
-				// move the directory containing the versions
340
-				$rootView->$operation(
341
-					'/' . $sourceOwner . '/files_versions/' . $sourcePath,
342
-					'/' . $targetOwner . '/files_versions/' . $targetPath
343
-				);
344
-			}
345
-		} elseif ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) {
346
-			// create missing dirs if necessary
347
-			self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
348
-
349
-			foreach ($versions as $v) {
350
-				// move each version one by one to the target directory
351
-				$rootView->$operation(
352
-					'/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'],
353
-					'/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version']
354
-				);
355
-			}
356
-		}
357
-
358
-		// if we moved versions directly for a file, schedule expiration check for that file
359
-		if (!$rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
360
-			self::scheduleExpire($targetOwner, $targetPath);
361
-		}
362
-	}
363
-
364
-	/**
365
-	 * Rollback to an old version of a file.
366
-	 *
367
-	 * @param string $file file name
368
-	 * @param int $revision revision timestamp
369
-	 * @return bool
370
-	 */
371
-	public static function rollback(string $file, int $revision, IUser $user) {
372
-		// add expected leading slash
373
-		$filename = '/' . ltrim($file, '/');
374
-
375
-		// Fetch the userfolder to trigger view hooks
376
-		$root = \OC::$server->get(IRootFolder::class);
377
-		$userFolder = $root->getUserFolder($user->getUID());
378
-
379
-		$users_view = new View('/'.$user->getUID());
380
-		$files_view = new View('/'. $user->getUID().'/files');
381
-
382
-		$versionCreated = false;
383
-
384
-		$fileInfo = $files_view->getFileInfo($file);
385
-
386
-		// check if user has the permissions to revert a version
387
-		if (!$fileInfo->isUpdateable()) {
388
-			return false;
389
-		}
390
-
391
-		//first create a new version
392
-		$version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename);
393
-		if (!$users_view->file_exists($version)) {
394
-			$users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
395
-			$versionCreated = true;
396
-		}
397
-
398
-		$fileToRestore = 'files_versions' . $filename . '.v' . $revision;
399
-
400
-		// Restore encrypted version of the old file for the newly restored file
401
-		// This has to happen manually here since the file is manually copied below
402
-		$oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion();
403
-		$oldFileInfo = $users_view->getFileInfo($fileToRestore);
404
-		$cache = $fileInfo->getStorage()->getCache();
405
-		$cache->update(
406
-			$fileInfo->getId(), [
407
-				'encrypted' => $oldVersion,
408
-				'encryptedVersion' => $oldVersion,
409
-				'size' => $oldFileInfo->getSize()
410
-			]
411
-		);
412
-
413
-		// rollback
414
-		if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) {
415
-			$files_view->touch($file, $revision);
416
-			Storage::scheduleExpire($user->getUID(), $file);
417
-
418
-			$node = $userFolder->get($file);
419
-
420
-			return true;
421
-		} elseif ($versionCreated) {
422
-			self::deleteVersion($users_view, $version);
423
-		}
424
-
425
-		return false;
426
-	}
427
-
428
-	/**
429
-	 * Stream copy file contents from $path1 to $path2
430
-	 *
431
-	 * @param View $view view to use for copying
432
-	 * @param string $path1 source file to copy
433
-	 * @param string $path2 target file
434
-	 *
435
-	 * @return bool true for success, false otherwise
436
-	 */
437
-	private static function copyFileContents($view, $path1, $path2) {
438
-		/** @var \OC\Files\Storage\Storage $storage1 */
439
-		[$storage1, $internalPath1] = $view->resolvePath($path1);
440
-		/** @var \OC\Files\Storage\Storage $storage2 */
441
-		[$storage2, $internalPath2] = $view->resolvePath($path2);
442
-
443
-		$view->lockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
444
-		$view->lockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
445
-
446
-		// TODO add a proper way of overwriting a file while maintaining file ids
447
-		if ($storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage') || $storage2->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')) {
448
-			$source = $storage1->fopen($internalPath1, 'r');
449
-			$target = $storage2->fopen($internalPath2, 'w');
450
-			[, $result] = \OC_Helper::streamCopy($source, $target);
451
-			fclose($source);
452
-			fclose($target);
453
-
454
-			if ($result !== false) {
455
-				$storage1->unlink($internalPath1);
456
-			}
457
-		} else {
458
-			$result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
459
-		}
460
-
461
-		$view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
462
-		$view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
463
-
464
-		return ($result !== false);
465
-	}
466
-
467
-	/**
468
-	 * get a list of all available versions of a file in descending chronological order
469
-	 * @param string $uid user id from the owner of the file
470
-	 * @param string $filename file to find versions of, relative to the user files dir
471
-	 * @param string $userFullPath
472
-	 * @return array versions newest version first
473
-	 */
474
-	public static function getVersions($uid, $filename, $userFullPath = '') {
475
-		$versions = [];
476
-		if (empty($filename)) {
477
-			return $versions;
478
-		}
479
-		// fetch for old versions
480
-		$view = new View('/' . $uid . '/');
481
-
482
-		$pathinfo = pathinfo($filename);
483
-		$versionedFile = $pathinfo['basename'];
484
-
485
-		$dir = Filesystem::normalizePath(self::VERSIONS_ROOT . '/' . $pathinfo['dirname']);
486
-
487
-		$dirContent = false;
488
-		if ($view->is_dir($dir)) {
489
-			$dirContent = $view->opendir($dir);
490
-		}
491
-
492
-		if ($dirContent === false) {
493
-			return $versions;
494
-		}
495
-
496
-		if (is_resource($dirContent)) {
497
-			while (($entryName = readdir($dirContent)) !== false) {
498
-				if (!Filesystem::isIgnoredDir($entryName)) {
499
-					$pathparts = pathinfo($entryName);
500
-					$filename = $pathparts['filename'];
501
-					if ($filename === $versionedFile) {
502
-						$pathparts = pathinfo($entryName);
503
-						$timestamp = substr($pathparts['extension'] ?? '', 1);
504
-						if (!is_numeric($timestamp)) {
505
-							\OC::$server->get(LoggerInterface::class)->error(
506
-								'Version file {path} has incorrect name format',
507
-								[
508
-									'path' => $entryName,
509
-									'app' => 'files_versions',
510
-								]
511
-							);
512
-							continue;
513
-						}
514
-						$filename = $pathparts['filename'];
515
-						$key = $timestamp . '#' . $filename;
516
-						$versions[$key]['version'] = $timestamp;
517
-						$versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp((int)$timestamp);
518
-						if (empty($userFullPath)) {
519
-							$versions[$key]['preview'] = '';
520
-						} else {
521
-							/** @var IURLGenerator $urlGenerator */
522
-							$urlGenerator = \OC::$server->get(IURLGenerator::class);
523
-							$versions[$key]['preview'] = $urlGenerator->linkToRoute('files_version.Preview.getPreview',
524
-								['file' => $userFullPath, 'version' => $timestamp]);
525
-						}
526
-						$versions[$key]['path'] = Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename);
527
-						$versions[$key]['name'] = $versionedFile;
528
-						$versions[$key]['size'] = $view->filesize($dir . '/' . $entryName);
529
-						$versions[$key]['mimetype'] = \OC::$server->get(IMimeTypeDetector::class)->detectPath($versionedFile);
530
-					}
531
-				}
532
-			}
533
-			closedir($dirContent);
534
-		}
535
-
536
-		// sort with newest version first
537
-		krsort($versions);
538
-
539
-		return $versions;
540
-	}
541
-
542
-	/**
543
-	 * Expire versions that older than max version retention time
544
-	 *
545
-	 * @param string $uid
546
-	 */
547
-	public static function expireOlderThanMaxForUser($uid) {
548
-		/** @var IRootFolder $root */
549
-		$root = \OC::$server->get(IRootFolder::class);
550
-		try {
551
-			/** @var Folder $versionsRoot */
552
-			$versionsRoot = $root->get('/' . $uid . '/files_versions');
553
-		} catch (NotFoundException $e) {
554
-			return;
555
-		}
556
-
557
-		$expiration = self::getExpiration();
558
-		$threshold = $expiration->getMaxAgeAsTimestamp();
559
-		if (!$threshold) {
560
-			return;
561
-		}
562
-
563
-		$allVersions = $versionsRoot->search(new SearchQuery(
564
-			new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
565
-				new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER),
566
-			]),
567
-			0,
568
-			0,
569
-			[]
570
-		));
571
-
572
-		/** @var VersionsMapper $versionsMapper */
573
-		$versionsMapper = \OC::$server->get(VersionsMapper::class);
574
-		$userFolder = $root->getUserFolder($uid);
575
-		$versionEntities = [];
576
-
577
-		/** @var Node[] $versions */
578
-		$versions = array_filter($allVersions, function (Node $info) use ($threshold, $userFolder, $versionsMapper, $versionsRoot, &$versionEntities) {
579
-			// Check that the file match '*.v*'
580
-			$versionsBegin = strrpos($info->getName(), '.v');
581
-			if ($versionsBegin === false) {
582
-				return false;
583
-			}
584
-
585
-			$version = (int)substr($info->getName(), $versionsBegin + 2);
586
-
587
-			// Check that the version does not have a label.
588
-			$path = $versionsRoot->getRelativePath($info->getPath());
589
-			$node = $userFolder->get(substr($path, 0, -strlen('.v'.$version)));
590
-			try {
591
-				$versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version);
592
-				$versionEntities[$info->getId()] = $versionEntity;
593
-
594
-				if ($versionEntity->getLabel() !== '') {
595
-					return false;
596
-				}
597
-			} catch (DoesNotExistException $ex) {
598
-				// Version on FS can have no equivalent in the DB if they were created before the version naming feature.
599
-				// So we ignore DoesNotExistException.
600
-			}
601
-
602
-			// Check that the version's timestamp is lower than $threshold
603
-			return $version < $threshold;
604
-		});
605
-
606
-		foreach ($versions as $version) {
607
-			$internalPath = $version->getInternalPath();
608
-			\OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
609
-			$versionsMapper->delete($versionEntities[$version->getId()]);
610
-			$version->delete();
611
-			\OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
612
-		}
613
-	}
614
-
615
-	/**
616
-	 * translate a timestamp into a string like "5 days ago"
617
-	 *
618
-	 * @param int $timestamp
619
-	 * @return string for example "5 days ago"
620
-	 */
621
-	private static function getHumanReadableTimestamp(int $timestamp): string {
622
-		$diff = time() - $timestamp;
623
-
624
-		if ($diff < 60) { // first minute
625
-			return  $diff . " seconds ago";
626
-		} elseif ($diff < 3600) { //first hour
627
-			return round($diff / 60) . " minutes ago";
628
-		} elseif ($diff < 86400) { // first day
629
-			return round($diff / 3600) . " hours ago";
630
-		} elseif ($diff < 604800) { //first week
631
-			return round($diff / 86400) . " days ago";
632
-		} elseif ($diff < 2419200) { //first month
633
-			return round($diff / 604800) . " weeks ago";
634
-		} elseif ($diff < 29030400) { // first year
635
-			return round($diff / 2419200) . " months ago";
636
-		} else {
637
-			return round($diff / 29030400) . " years ago";
638
-		}
639
-	}
640
-
641
-	/**
642
-	 * returns all stored file versions from a given user
643
-	 * @param string $uid id of the user
644
-	 * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
645
-	 */
646
-	private static function getAllVersions($uid) {
647
-		$view = new View('/' . $uid . '/');
648
-		$dirs = [self::VERSIONS_ROOT];
649
-		$versions = [];
650
-
651
-		while (!empty($dirs)) {
652
-			$dir = array_pop($dirs);
653
-			$files = $view->getDirectoryContent($dir);
654
-
655
-			foreach ($files as $file) {
656
-				$fileData = $file->getData();
657
-				$filePath = $dir . '/' . $fileData['name'];
658
-				if ($file['type'] === 'dir') {
659
-					$dirs[] = $filePath;
660
-				} else {
661
-					$versionsBegin = strrpos($filePath, '.v');
662
-					$relPathStart = strlen(self::VERSIONS_ROOT);
663
-					$version = substr($filePath, $versionsBegin + 2);
664
-					$relpath = substr($filePath, $relPathStart, $versionsBegin - $relPathStart);
665
-					$key = $version . '#' . $relpath;
666
-					$versions[$key] = ['path' => $relpath, 'timestamp' => $version];
667
-				}
668
-			}
669
-		}
670
-
671
-		// newest version first
672
-		krsort($versions);
673
-
674
-		$result = [
675
-			'all' => [],
676
-			'by_file' => [],
677
-		];
678
-
679
-		foreach ($versions as $key => $value) {
680
-			$size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']);
681
-			$filename = $value['path'];
682
-
683
-			$result['all'][$key]['version'] = $value['timestamp'];
684
-			$result['all'][$key]['path'] = $filename;
685
-			$result['all'][$key]['size'] = $size;
686
-
687
-			$result['by_file'][$filename][$key]['version'] = $value['timestamp'];
688
-			$result['by_file'][$filename][$key]['path'] = $filename;
689
-			$result['by_file'][$filename][$key]['size'] = $size;
690
-		}
691
-
692
-		return $result;
693
-	}
694
-
695
-	/**
696
-	 * get list of files we want to expire
697
-	 * @param array $versions list of versions
698
-	 * @param integer $time
699
-	 * @param bool $quotaExceeded is versions storage limit reached
700
-	 * @return array containing the list of to deleted versions and the size of them
701
-	 */
702
-	protected static function getExpireList($time, $versions, $quotaExceeded = false) {
703
-		$expiration = self::getExpiration();
704
-
705
-		if ($expiration->shouldAutoExpire()) {
706
-			[$toDelete, $size] = self::getAutoExpireList($time, $versions);
707
-		} else {
708
-			$size = 0;
709
-			$toDelete = [];  // versions we want to delete
710
-		}
711
-
712
-		foreach ($versions as $key => $version) {
713
-			if ($expiration->isExpired($version['version'], $quotaExceeded) && !isset($toDelete[$key])) {
714
-				$size += $version['size'];
715
-				$toDelete[$key] = $version['path'] . '.v' . $version['version'];
716
-			}
717
-		}
718
-
719
-		return [$toDelete, $size];
720
-	}
721
-
722
-	/**
723
-	 * get list of files we want to expire
724
-	 * @param array $versions list of versions
725
-	 * @param integer $time
726
-	 * @return array containing the list of to deleted versions and the size of them
727
-	 */
728
-	protected static function getAutoExpireList($time, $versions) {
729
-		$size = 0;
730
-		$toDelete = [];  // versions we want to delete
731
-
732
-		$interval = 1;
733
-		$step = Storage::$max_versions_per_interval[$interval]['step'];
734
-		if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
735
-			$nextInterval = -1;
736
-		} else {
737
-			$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
738
-		}
739
-
740
-		$firstVersion = reset($versions);
741
-
742
-		if ($firstVersion === false) {
743
-			return [$toDelete, $size];
744
-		}
745
-
746
-		$firstKey = key($versions);
747
-		$prevTimestamp = $firstVersion['version'];
748
-		$nextVersion = $firstVersion['version'] - $step;
749
-		unset($versions[$firstKey]);
750
-
751
-		foreach ($versions as $key => $version) {
752
-			$newInterval = true;
753
-			while ($newInterval) {
754
-				if ($nextInterval === -1 || $prevTimestamp > $nextInterval) {
755
-					if ($version['version'] > $nextVersion) {
756
-						//distance between two version too small, mark to delete
757
-						$toDelete[$key] = $version['path'] . '.v' . $version['version'];
758
-						$size += $version['size'];
759
-						\OC::$server->get(LoggerInterface::class)->info('Mark to expire '. $version['path'] .' next version should be ' . $nextVersion . " or smaller. (prevTimestamp: " . $prevTimestamp . "; step: " . $step, ['app' => 'files_versions']);
760
-					} else {
761
-						$nextVersion = $version['version'] - $step;
762
-						$prevTimestamp = $version['version'];
763
-					}
764
-					$newInterval = false; // version checked so we can move to the next one
765
-				} else { // time to move on to the next interval
766
-					$interval++;
767
-					$step = Storage::$max_versions_per_interval[$interval]['step'];
768
-					$nextVersion = $prevTimestamp - $step;
769
-					if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
770
-						$nextInterval = -1;
771
-					} else {
772
-						$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
773
-					}
774
-					$newInterval = true; // we changed the interval -> check same version with new interval
775
-				}
776
-			}
777
-		}
778
-
779
-		return [$toDelete, $size];
780
-	}
781
-
782
-	/**
783
-	 * Schedule versions expiration for the given file
784
-	 *
785
-	 * @param string $uid owner of the file
786
-	 * @param string $fileName file/folder for which to schedule expiration
787
-	 */
788
-	public static function scheduleExpire($uid, $fileName) {
789
-		// let the admin disable auto expire
790
-		$expiration = self::getExpiration();
791
-		if ($expiration->isEnabled()) {
792
-			$command = new Expire($uid, $fileName);
793
-			/** @var IBus $bus */
794
-			$bus = \OC::$server->get(IBus::class);
795
-			$bus->push($command);
796
-		}
797
-	}
798
-
799
-	/**
800
-	 * Expire versions which exceed the quota.
801
-	 *
802
-	 * This will setup the filesystem for the given user but will not
803
-	 * tear it down afterwards.
804
-	 *
805
-	 * @param string $filename path to file to expire
806
-	 * @param string $uid user for which to expire the version
807
-	 * @return bool|int|null
808
-	 */
809
-	public static function expire($filename, $uid) {
810
-		$expiration = self::getExpiration();
811
-
812
-		/** @var LoggerInterface $logger */
813
-		$logger = \OC::$server->get(LoggerInterface::class);
814
-
815
-		if ($expiration->isEnabled()) {
816
-			// get available disk space for user
817
-			$user = \OC::$server->get(IUserManager::class)->get($uid);
818
-			if (is_null($user)) {
819
-				$logger->error('Backends provided no user object for ' . $uid, ['app' => 'files_versions']);
820
-				throw new \OC\User\NoUserException('Backends provided no user object for ' . $uid);
821
-			}
822
-
823
-			\OC_Util::setupFS($uid);
824
-
825
-			try {
826
-				if (!Filesystem::file_exists($filename)) {
827
-					return false;
828
-				}
829
-			} catch (StorageNotAvailableException $e) {
830
-				// if we can't check that the file hasn't been deleted we can only assume that it hasn't
831
-				// note that this `StorageNotAvailableException` is about the file the versions originate from,
832
-				// not the storage that the versions are stored on
833
-			}
834
-
835
-			if (empty($filename)) {
836
-				// file maybe renamed or deleted
837
-				return false;
838
-			}
839
-			$versionsFileview = new View('/'.$uid.'/files_versions');
840
-
841
-			$softQuota = true;
842
-			$quota = $user->getQuota();
843
-			if ($quota === null || $quota === 'none') {
844
-				$quota = Filesystem::free_space('/');
845
-				$softQuota = false;
846
-			} else {
847
-				$quota = \OCP\Util::computerFileSize($quota);
848
-			}
849
-
850
-			// make sure that we have the current size of the version history
851
-			$versionsSize = self::getVersionsSize($uid);
852
-
853
-			// calculate available space for version history
854
-			// subtract size of files and current versions size from quota
855
-			if ($quota >= 0) {
856
-				if ($softQuota) {
857
-					$root = \OC::$server->get(IRootFolder::class);
858
-					$userFolder = $root->getUserFolder($uid);
859
-					if (is_null($userFolder)) {
860
-						$availableSpace = 0;
861
-					} else {
862
-						$free = $quota - $userFolder->getSize(false); // remaining free space for user
863
-						if ($free > 0) {
864
-							$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $versionsSize; // how much space can be used for versions
865
-						} else {
866
-							$availableSpace = $free - $versionsSize;
867
-						}
868
-					}
869
-				} else {
870
-					$availableSpace = $quota;
871
-				}
872
-			} else {
873
-				$availableSpace = PHP_INT_MAX;
874
-			}
875
-
876
-			$allVersions = Storage::getVersions($uid, $filename);
877
-
878
-			$time = time();
879
-			[$toDelete, $sizeOfDeletedVersions] = self::getExpireList($time, $allVersions, $availableSpace <= 0);
880
-
881
-			$availableSpace = $availableSpace + $sizeOfDeletedVersions;
882
-			$versionsSize = $versionsSize - $sizeOfDeletedVersions;
883
-
884
-			// if still not enough free space we rearrange the versions from all files
885
-			if ($availableSpace <= 0) {
886
-				$result = self::getAllVersions($uid);
887
-				$allVersions = $result['all'];
888
-
889
-				foreach ($result['by_file'] as $versions) {
890
-					[$toDeleteNew, $size] = self::getExpireList($time, $versions, $availableSpace <= 0);
891
-					$toDelete = array_merge($toDelete, $toDeleteNew);
892
-					$sizeOfDeletedVersions += $size;
893
-				}
894
-				$availableSpace = $availableSpace + $sizeOfDeletedVersions;
895
-				$versionsSize = $versionsSize - $sizeOfDeletedVersions;
896
-			}
897
-
898
-			foreach ($toDelete as $key => $path) {
899
-				\OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
900
-				self::deleteVersion($versionsFileview, $path);
901
-				\OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
902
-				unset($allVersions[$key]); // update array with the versions we keep
903
-				$logger->info('Expire: ' . $path, ['app' => 'files_versions']);
904
-			}
905
-
906
-			// Check if enough space is available after versions are rearranged.
907
-			// If not we delete the oldest versions until we meet the size limit for versions,
908
-			// but always keep the two latest versions
909
-			$numOfVersions = count($allVersions) - 2 ;
910
-			$i = 0;
911
-			// sort oldest first and make sure that we start at the first element
912
-			ksort($allVersions);
913
-			reset($allVersions);
914
-			while ($availableSpace < 0 && $i < $numOfVersions) {
915
-				$version = current($allVersions);
916
-				\OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
917
-				self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']);
918
-				\OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
919
-				$logger->info('running out of space! Delete oldest version: ' . $version['path'].'.v'.$version['version'], ['app' => 'files_versions']);
920
-				$versionsSize -= $version['size'];
921
-				$availableSpace += $version['size'];
922
-				next($allVersions);
923
-				$i++;
924
-			}
925
-
926
-			return $versionsSize; // finally return the new size of the version history
927
-		}
928
-
929
-		return false;
930
-	}
931
-
932
-	/**
933
-	 * Create recursively missing directories inside of files_versions
934
-	 * that match the given path to a file.
935
-	 *
936
-	 * @param string $filename $path to a file, relative to the user's
937
-	 * "files" folder
938
-	 * @param View $view view on data/user/
939
-	 */
940
-	public static function createMissingDirectories($filename, $view) {
941
-		$dirname = Filesystem::normalizePath(dirname($filename));
942
-		$dirParts = explode('/', $dirname);
943
-		$dir = "/files_versions";
944
-		foreach ($dirParts as $part) {
945
-			$dir = $dir . '/' . $part;
946
-			if (!$view->file_exists($dir)) {
947
-				$view->mkdir($dir);
948
-			}
949
-		}
950
-	}
951
-
952
-	/**
953
-	 * Static workaround
954
-	 * @return Expiration
955
-	 */
956
-	protected static function getExpiration() {
957
-		if (self::$application === null) {
958
-			self::$application = \OC::$server->get(Application::class);
959
-		}
960
-		return self::$application->getContainer()->get(Expiration::class);
961
-	}
74
+    public const DEFAULTENABLED = true;
75
+    public const DEFAULTMAXSIZE = 50; // unit: percentage; 50% of available disk space/quota
76
+    public const VERSIONS_ROOT = 'files_versions/';
77
+
78
+    public const DELETE_TRIGGER_MASTER_REMOVED = 0;
79
+    public const DELETE_TRIGGER_RETENTION_CONSTRAINT = 1;
80
+    public const DELETE_TRIGGER_QUOTA_EXCEEDED = 2;
81
+
82
+    // files for which we can remove the versions after the delete operation was successful
83
+    private static $deletedFiles = [];
84
+
85
+    private static $sourcePathAndUser = [];
86
+
87
+    private static $max_versions_per_interval = [
88
+        //first 10sec, one version every 2sec
89
+        1 => ['intervalEndsAfter' => 10,      'step' => 2],
90
+        //next minute, one version every 10sec
91
+        2 => ['intervalEndsAfter' => 60,      'step' => 10],
92
+        //next hour, one version every minute
93
+        3 => ['intervalEndsAfter' => 3600,    'step' => 60],
94
+        //next 24h, one version every hour
95
+        4 => ['intervalEndsAfter' => 86400,   'step' => 3600],
96
+        //next 30days, one version per day
97
+        5 => ['intervalEndsAfter' => 2592000, 'step' => 86400],
98
+        //until the end one version per week
99
+        6 => ['intervalEndsAfter' => -1,      'step' => 604800],
100
+    ];
101
+
102
+    /** @var \OCA\Files_Versions\AppInfo\Application */
103
+    private static $application;
104
+
105
+    /**
106
+     * get the UID of the owner of the file and the path to the file relative to
107
+     * owners files folder
108
+     *
109
+     * @param string $filename
110
+     * @return array
111
+     * @throws \OC\User\NoUserException
112
+     */
113
+    public static function getUidAndFilename($filename) {
114
+        $uid = Filesystem::getOwner($filename);
115
+        $userManager = \OC::$server->get(IUserManager::class);
116
+        // if the user with the UID doesn't exists, e.g. because the UID points
117
+        // to a remote user with a federated cloud ID we use the current logged-in
118
+        // user. We need a valid local user to create the versions
119
+        if (!$userManager->userExists($uid)) {
120
+            $uid = OC_User::getUser();
121
+        }
122
+        Filesystem::initMountPoints($uid);
123
+        if ($uid !== OC_User::getUser()) {
124
+            $info = Filesystem::getFileInfo($filename);
125
+            $ownerView = new View('/'.$uid.'/files');
126
+            try {
127
+                $filename = $ownerView->getPath($info['fileid']);
128
+                // make sure that the file name doesn't end with a trailing slash
129
+                // can for example happen single files shared across servers
130
+                $filename = rtrim($filename, '/');
131
+            } catch (NotFoundException $e) {
132
+                $filename = null;
133
+            }
134
+        }
135
+        return [$uid, $filename];
136
+    }
137
+
138
+    /**
139
+     * Remember the owner and the owner path of the source file
140
+     *
141
+     * @param string $source source path
142
+     */
143
+    public static function setSourcePathAndUser($source) {
144
+        [$uid, $path] = self::getUidAndFilename($source);
145
+        self::$sourcePathAndUser[$source] = ['uid' => $uid, 'path' => $path];
146
+    }
147
+
148
+    /**
149
+     * Gets the owner and the owner path from the source path
150
+     *
151
+     * @param string $source source path
152
+     * @return array with user id and path
153
+     */
154
+    public static function getSourcePathAndUser($source) {
155
+        if (isset(self::$sourcePathAndUser[$source])) {
156
+            $uid = self::$sourcePathAndUser[$source]['uid'];
157
+            $path = self::$sourcePathAndUser[$source]['path'];
158
+            unset(self::$sourcePathAndUser[$source]);
159
+        } else {
160
+            $uid = $path = false;
161
+        }
162
+        return [$uid, $path];
163
+    }
164
+
165
+    /**
166
+     * get current size of all versions from a given user
167
+     *
168
+     * @param string $user user who owns the versions
169
+     * @return int versions size
170
+     */
171
+    private static function getVersionsSize($user) {
172
+        $view = new View('/' . $user);
173
+        $fileInfo = $view->getFileInfo('/files_versions');
174
+        return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
175
+    }
176
+
177
+    /**
178
+     * store a new version of a file.
179
+     */
180
+    public static function store($filename) {
181
+        // if the file gets streamed we need to remove the .part extension
182
+        // to get the right target
183
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
184
+        if ($ext === 'part') {
185
+            $filename = substr($filename, 0, -5);
186
+        }
187
+
188
+        // we only handle existing files
189
+        if (! Filesystem::file_exists($filename) || Filesystem::is_dir($filename)) {
190
+            return false;
191
+        }
192
+
193
+        // since hook paths are always relative to the "default filesystem view"
194
+        // we always use the owner from there to get the full node
195
+        $uid = Filesystem::getView()->getOwner('');
196
+
197
+        /** @var IRootFolder $rootFolder */
198
+        $rootFolder = \OC::$server->get(IRootFolder::class);
199
+        $userFolder = $rootFolder->getUserFolder($uid);
200
+
201
+        $eventDispatcher = \OC::$server->get(IEventDispatcher::class);
202
+        try {
203
+            $file = $userFolder->get($filename);
204
+        } catch (NotFoundException $e) {
205
+            return false;
206
+        }
207
+
208
+        $mount = $file->getMountPoint();
209
+        if ($mount instanceof SharedMount) {
210
+            $ownerFolder = $rootFolder->getUserFolder($mount->getShare()->getShareOwner());
211
+            $ownerNodes = $ownerFolder->getById($file->getId());
212
+            if (count($ownerNodes)) {
213
+                $file = current($ownerNodes);
214
+                $uid = $mount->getShare()->getShareOwner();
215
+            }
216
+        }
217
+
218
+        /** @var IUserManager $userManager */
219
+        $userManager = \OC::$server->get(IUserManager::class);
220
+        $user = $userManager->get($uid);
221
+
222
+        if (!$user) {
223
+            return false;
224
+        }
225
+
226
+        // no use making versions for empty files
227
+        if ($file->getSize() === 0) {
228
+            return false;
229
+        }
230
+
231
+        $event = new CreateVersionEvent($file);
232
+        $eventDispatcher->dispatch('OCA\Files_Versions::createVersion', $event);
233
+        if ($event->shouldCreateVersion() === false) {
234
+            return false;
235
+        }
236
+
237
+        /** @var IVersionManager $versionManager */
238
+        $versionManager = \OC::$server->get(IVersionManager::class);
239
+
240
+        $versionManager->createVersion($user, $file);
241
+    }
242
+
243
+
244
+    /**
245
+     * mark file as deleted so that we can remove the versions if the file is gone
246
+     * @param string $path
247
+     */
248
+    public static function markDeletedFile($path) {
249
+        [$uid, $filename] = self::getUidAndFilename($path);
250
+        self::$deletedFiles[$path] = [
251
+            'uid' => $uid,
252
+            'filename' => $filename];
253
+    }
254
+
255
+    /**
256
+     * delete the version from the storage and cache
257
+     *
258
+     * @param View $view
259
+     * @param string $path
260
+     */
261
+    protected static function deleteVersion($view, $path) {
262
+        $view->unlink($path);
263
+        /**
264
+         * @var \OC\Files\Storage\Storage $storage
265
+         * @var string $internalPath
266
+         */
267
+        [$storage, $internalPath] = $view->resolvePath($path);
268
+        $cache = $storage->getCache($internalPath);
269
+        $cache->remove($internalPath);
270
+    }
271
+
272
+    /**
273
+     * Delete versions of a file
274
+     */
275
+    public static function delete($path) {
276
+        $deletedFile = self::$deletedFiles[$path];
277
+        $uid = $deletedFile['uid'];
278
+        $filename = $deletedFile['filename'];
279
+
280
+        if (!Filesystem::file_exists($path)) {
281
+            $view = new View('/' . $uid . '/files_versions');
282
+
283
+            $versions = self::getVersions($uid, $filename);
284
+            if (!empty($versions)) {
285
+                foreach ($versions as $v) {
286
+                    \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
287
+                    self::deleteVersion($view, $filename . '.v' . $v['version']);
288
+                    \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $v['version'], 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
289
+                }
290
+            }
291
+        }
292
+        unset(self::$deletedFiles[$path]);
293
+    }
294
+
295
+    /**
296
+     * Delete a version of a file
297
+     */
298
+    public static function deleteRevision(string $path, int $revision): void {
299
+        [$uid, $filename] = self::getUidAndFilename($path);
300
+        $view = new View('/' . $uid . '/files_versions');
301
+        \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
302
+        self::deleteVersion($view, $filename . '.v' . $revision);
303
+        \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path . $revision, 'trigger' => self::DELETE_TRIGGER_MASTER_REMOVED]);
304
+    }
305
+
306
+    /**
307
+     * Rename or copy versions of a file of the given paths
308
+     *
309
+     * @param string $sourcePath source path of the file to move, relative to
310
+     * the currently logged in user's "files" folder
311
+     * @param string $targetPath target path of the file to move, relative to
312
+     * the currently logged in user's "files" folder
313
+     * @param string $operation can be 'copy' or 'rename'
314
+     */
315
+    public static function renameOrCopy($sourcePath, $targetPath, $operation) {
316
+        [$sourceOwner, $sourcePath] = self::getSourcePathAndUser($sourcePath);
317
+
318
+        // it was a upload of a existing file if no old path exists
319
+        // in this case the pre-hook already called the store method and we can
320
+        // stop here
321
+        if ($sourcePath === false) {
322
+            return true;
323
+        }
324
+
325
+        [$targetOwner, $targetPath] = self::getUidAndFilename($targetPath);
326
+
327
+        $sourcePath = ltrim($sourcePath, '/');
328
+        $targetPath = ltrim($targetPath, '/');
329
+
330
+        $rootView = new View('');
331
+
332
+        // did we move a directory ?
333
+        if ($rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
334
+            // does the directory exists for versions too ?
335
+            if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) {
336
+                // create missing dirs if necessary
337
+                self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
338
+
339
+                // move the directory containing the versions
340
+                $rootView->$operation(
341
+                    '/' . $sourceOwner . '/files_versions/' . $sourcePath,
342
+                    '/' . $targetOwner . '/files_versions/' . $targetPath
343
+                );
344
+            }
345
+        } elseif ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) {
346
+            // create missing dirs if necessary
347
+            self::createMissingDirectories($targetPath, new View('/'. $targetOwner));
348
+
349
+            foreach ($versions as $v) {
350
+                // move each version one by one to the target directory
351
+                $rootView->$operation(
352
+                    '/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'],
353
+                    '/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version']
354
+                );
355
+            }
356
+        }
357
+
358
+        // if we moved versions directly for a file, schedule expiration check for that file
359
+        if (!$rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
360
+            self::scheduleExpire($targetOwner, $targetPath);
361
+        }
362
+    }
363
+
364
+    /**
365
+     * Rollback to an old version of a file.
366
+     *
367
+     * @param string $file file name
368
+     * @param int $revision revision timestamp
369
+     * @return bool
370
+     */
371
+    public static function rollback(string $file, int $revision, IUser $user) {
372
+        // add expected leading slash
373
+        $filename = '/' . ltrim($file, '/');
374
+
375
+        // Fetch the userfolder to trigger view hooks
376
+        $root = \OC::$server->get(IRootFolder::class);
377
+        $userFolder = $root->getUserFolder($user->getUID());
378
+
379
+        $users_view = new View('/'.$user->getUID());
380
+        $files_view = new View('/'. $user->getUID().'/files');
381
+
382
+        $versionCreated = false;
383
+
384
+        $fileInfo = $files_view->getFileInfo($file);
385
+
386
+        // check if user has the permissions to revert a version
387
+        if (!$fileInfo->isUpdateable()) {
388
+            return false;
389
+        }
390
+
391
+        //first create a new version
392
+        $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename);
393
+        if (!$users_view->file_exists($version)) {
394
+            $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
395
+            $versionCreated = true;
396
+        }
397
+
398
+        $fileToRestore = 'files_versions' . $filename . '.v' . $revision;
399
+
400
+        // Restore encrypted version of the old file for the newly restored file
401
+        // This has to happen manually here since the file is manually copied below
402
+        $oldVersion = $users_view->getFileInfo($fileToRestore)->getEncryptedVersion();
403
+        $oldFileInfo = $users_view->getFileInfo($fileToRestore);
404
+        $cache = $fileInfo->getStorage()->getCache();
405
+        $cache->update(
406
+            $fileInfo->getId(), [
407
+                'encrypted' => $oldVersion,
408
+                'encryptedVersion' => $oldVersion,
409
+                'size' => $oldFileInfo->getSize()
410
+            ]
411
+        );
412
+
413
+        // rollback
414
+        if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) {
415
+            $files_view->touch($file, $revision);
416
+            Storage::scheduleExpire($user->getUID(), $file);
417
+
418
+            $node = $userFolder->get($file);
419
+
420
+            return true;
421
+        } elseif ($versionCreated) {
422
+            self::deleteVersion($users_view, $version);
423
+        }
424
+
425
+        return false;
426
+    }
427
+
428
+    /**
429
+     * Stream copy file contents from $path1 to $path2
430
+     *
431
+     * @param View $view view to use for copying
432
+     * @param string $path1 source file to copy
433
+     * @param string $path2 target file
434
+     *
435
+     * @return bool true for success, false otherwise
436
+     */
437
+    private static function copyFileContents($view, $path1, $path2) {
438
+        /** @var \OC\Files\Storage\Storage $storage1 */
439
+        [$storage1, $internalPath1] = $view->resolvePath($path1);
440
+        /** @var \OC\Files\Storage\Storage $storage2 */
441
+        [$storage2, $internalPath2] = $view->resolvePath($path2);
442
+
443
+        $view->lockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
444
+        $view->lockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
445
+
446
+        // TODO add a proper way of overwriting a file while maintaining file ids
447
+        if ($storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage') || $storage2->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')) {
448
+            $source = $storage1->fopen($internalPath1, 'r');
449
+            $target = $storage2->fopen($internalPath2, 'w');
450
+            [, $result] = \OC_Helper::streamCopy($source, $target);
451
+            fclose($source);
452
+            fclose($target);
453
+
454
+            if ($result !== false) {
455
+                $storage1->unlink($internalPath1);
456
+            }
457
+        } else {
458
+            $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2);
459
+        }
460
+
461
+        $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE);
462
+        $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE);
463
+
464
+        return ($result !== false);
465
+    }
466
+
467
+    /**
468
+     * get a list of all available versions of a file in descending chronological order
469
+     * @param string $uid user id from the owner of the file
470
+     * @param string $filename file to find versions of, relative to the user files dir
471
+     * @param string $userFullPath
472
+     * @return array versions newest version first
473
+     */
474
+    public static function getVersions($uid, $filename, $userFullPath = '') {
475
+        $versions = [];
476
+        if (empty($filename)) {
477
+            return $versions;
478
+        }
479
+        // fetch for old versions
480
+        $view = new View('/' . $uid . '/');
481
+
482
+        $pathinfo = pathinfo($filename);
483
+        $versionedFile = $pathinfo['basename'];
484
+
485
+        $dir = Filesystem::normalizePath(self::VERSIONS_ROOT . '/' . $pathinfo['dirname']);
486
+
487
+        $dirContent = false;
488
+        if ($view->is_dir($dir)) {
489
+            $dirContent = $view->opendir($dir);
490
+        }
491
+
492
+        if ($dirContent === false) {
493
+            return $versions;
494
+        }
495
+
496
+        if (is_resource($dirContent)) {
497
+            while (($entryName = readdir($dirContent)) !== false) {
498
+                if (!Filesystem::isIgnoredDir($entryName)) {
499
+                    $pathparts = pathinfo($entryName);
500
+                    $filename = $pathparts['filename'];
501
+                    if ($filename === $versionedFile) {
502
+                        $pathparts = pathinfo($entryName);
503
+                        $timestamp = substr($pathparts['extension'] ?? '', 1);
504
+                        if (!is_numeric($timestamp)) {
505
+                            \OC::$server->get(LoggerInterface::class)->error(
506
+                                'Version file {path} has incorrect name format',
507
+                                [
508
+                                    'path' => $entryName,
509
+                                    'app' => 'files_versions',
510
+                                ]
511
+                            );
512
+                            continue;
513
+                        }
514
+                        $filename = $pathparts['filename'];
515
+                        $key = $timestamp . '#' . $filename;
516
+                        $versions[$key]['version'] = $timestamp;
517
+                        $versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp((int)$timestamp);
518
+                        if (empty($userFullPath)) {
519
+                            $versions[$key]['preview'] = '';
520
+                        } else {
521
+                            /** @var IURLGenerator $urlGenerator */
522
+                            $urlGenerator = \OC::$server->get(IURLGenerator::class);
523
+                            $versions[$key]['preview'] = $urlGenerator->linkToRoute('files_version.Preview.getPreview',
524
+                                ['file' => $userFullPath, 'version' => $timestamp]);
525
+                        }
526
+                        $versions[$key]['path'] = Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename);
527
+                        $versions[$key]['name'] = $versionedFile;
528
+                        $versions[$key]['size'] = $view->filesize($dir . '/' . $entryName);
529
+                        $versions[$key]['mimetype'] = \OC::$server->get(IMimeTypeDetector::class)->detectPath($versionedFile);
530
+                    }
531
+                }
532
+            }
533
+            closedir($dirContent);
534
+        }
535
+
536
+        // sort with newest version first
537
+        krsort($versions);
538
+
539
+        return $versions;
540
+    }
541
+
542
+    /**
543
+     * Expire versions that older than max version retention time
544
+     *
545
+     * @param string $uid
546
+     */
547
+    public static function expireOlderThanMaxForUser($uid) {
548
+        /** @var IRootFolder $root */
549
+        $root = \OC::$server->get(IRootFolder::class);
550
+        try {
551
+            /** @var Folder $versionsRoot */
552
+            $versionsRoot = $root->get('/' . $uid . '/files_versions');
553
+        } catch (NotFoundException $e) {
554
+            return;
555
+        }
556
+
557
+        $expiration = self::getExpiration();
558
+        $threshold = $expiration->getMaxAgeAsTimestamp();
559
+        if (!$threshold) {
560
+            return;
561
+        }
562
+
563
+        $allVersions = $versionsRoot->search(new SearchQuery(
564
+            new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [
565
+                new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER),
566
+            ]),
567
+            0,
568
+            0,
569
+            []
570
+        ));
571
+
572
+        /** @var VersionsMapper $versionsMapper */
573
+        $versionsMapper = \OC::$server->get(VersionsMapper::class);
574
+        $userFolder = $root->getUserFolder($uid);
575
+        $versionEntities = [];
576
+
577
+        /** @var Node[] $versions */
578
+        $versions = array_filter($allVersions, function (Node $info) use ($threshold, $userFolder, $versionsMapper, $versionsRoot, &$versionEntities) {
579
+            // Check that the file match '*.v*'
580
+            $versionsBegin = strrpos($info->getName(), '.v');
581
+            if ($versionsBegin === false) {
582
+                return false;
583
+            }
584
+
585
+            $version = (int)substr($info->getName(), $versionsBegin + 2);
586
+
587
+            // Check that the version does not have a label.
588
+            $path = $versionsRoot->getRelativePath($info->getPath());
589
+            $node = $userFolder->get(substr($path, 0, -strlen('.v'.$version)));
590
+            try {
591
+                $versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version);
592
+                $versionEntities[$info->getId()] = $versionEntity;
593
+
594
+                if ($versionEntity->getLabel() !== '') {
595
+                    return false;
596
+                }
597
+            } catch (DoesNotExistException $ex) {
598
+                // Version on FS can have no equivalent in the DB if they were created before the version naming feature.
599
+                // So we ignore DoesNotExistException.
600
+            }
601
+
602
+            // Check that the version's timestamp is lower than $threshold
603
+            return $version < $threshold;
604
+        });
605
+
606
+        foreach ($versions as $version) {
607
+            $internalPath = $version->getInternalPath();
608
+            \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
609
+            $versionsMapper->delete($versionEntities[$version->getId()]);
610
+            $version->delete();
611
+            \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]);
612
+        }
613
+    }
614
+
615
+    /**
616
+     * translate a timestamp into a string like "5 days ago"
617
+     *
618
+     * @param int $timestamp
619
+     * @return string for example "5 days ago"
620
+     */
621
+    private static function getHumanReadableTimestamp(int $timestamp): string {
622
+        $diff = time() - $timestamp;
623
+
624
+        if ($diff < 60) { // first minute
625
+            return  $diff . " seconds ago";
626
+        } elseif ($diff < 3600) { //first hour
627
+            return round($diff / 60) . " minutes ago";
628
+        } elseif ($diff < 86400) { // first day
629
+            return round($diff / 3600) . " hours ago";
630
+        } elseif ($diff < 604800) { //first week
631
+            return round($diff / 86400) . " days ago";
632
+        } elseif ($diff < 2419200) { //first month
633
+            return round($diff / 604800) . " weeks ago";
634
+        } elseif ($diff < 29030400) { // first year
635
+            return round($diff / 2419200) . " months ago";
636
+        } else {
637
+            return round($diff / 29030400) . " years ago";
638
+        }
639
+    }
640
+
641
+    /**
642
+     * returns all stored file versions from a given user
643
+     * @param string $uid id of the user
644
+     * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
645
+     */
646
+    private static function getAllVersions($uid) {
647
+        $view = new View('/' . $uid . '/');
648
+        $dirs = [self::VERSIONS_ROOT];
649
+        $versions = [];
650
+
651
+        while (!empty($dirs)) {
652
+            $dir = array_pop($dirs);
653
+            $files = $view->getDirectoryContent($dir);
654
+
655
+            foreach ($files as $file) {
656
+                $fileData = $file->getData();
657
+                $filePath = $dir . '/' . $fileData['name'];
658
+                if ($file['type'] === 'dir') {
659
+                    $dirs[] = $filePath;
660
+                } else {
661
+                    $versionsBegin = strrpos($filePath, '.v');
662
+                    $relPathStart = strlen(self::VERSIONS_ROOT);
663
+                    $version = substr($filePath, $versionsBegin + 2);
664
+                    $relpath = substr($filePath, $relPathStart, $versionsBegin - $relPathStart);
665
+                    $key = $version . '#' . $relpath;
666
+                    $versions[$key] = ['path' => $relpath, 'timestamp' => $version];
667
+                }
668
+            }
669
+        }
670
+
671
+        // newest version first
672
+        krsort($versions);
673
+
674
+        $result = [
675
+            'all' => [],
676
+            'by_file' => [],
677
+        ];
678
+
679
+        foreach ($versions as $key => $value) {
680
+            $size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']);
681
+            $filename = $value['path'];
682
+
683
+            $result['all'][$key]['version'] = $value['timestamp'];
684
+            $result['all'][$key]['path'] = $filename;
685
+            $result['all'][$key]['size'] = $size;
686
+
687
+            $result['by_file'][$filename][$key]['version'] = $value['timestamp'];
688
+            $result['by_file'][$filename][$key]['path'] = $filename;
689
+            $result['by_file'][$filename][$key]['size'] = $size;
690
+        }
691
+
692
+        return $result;
693
+    }
694
+
695
+    /**
696
+     * get list of files we want to expire
697
+     * @param array $versions list of versions
698
+     * @param integer $time
699
+     * @param bool $quotaExceeded is versions storage limit reached
700
+     * @return array containing the list of to deleted versions and the size of them
701
+     */
702
+    protected static function getExpireList($time, $versions, $quotaExceeded = false) {
703
+        $expiration = self::getExpiration();
704
+
705
+        if ($expiration->shouldAutoExpire()) {
706
+            [$toDelete, $size] = self::getAutoExpireList($time, $versions);
707
+        } else {
708
+            $size = 0;
709
+            $toDelete = [];  // versions we want to delete
710
+        }
711
+
712
+        foreach ($versions as $key => $version) {
713
+            if ($expiration->isExpired($version['version'], $quotaExceeded) && !isset($toDelete[$key])) {
714
+                $size += $version['size'];
715
+                $toDelete[$key] = $version['path'] . '.v' . $version['version'];
716
+            }
717
+        }
718
+
719
+        return [$toDelete, $size];
720
+    }
721
+
722
+    /**
723
+     * get list of files we want to expire
724
+     * @param array $versions list of versions
725
+     * @param integer $time
726
+     * @return array containing the list of to deleted versions and the size of them
727
+     */
728
+    protected static function getAutoExpireList($time, $versions) {
729
+        $size = 0;
730
+        $toDelete = [];  // versions we want to delete
731
+
732
+        $interval = 1;
733
+        $step = Storage::$max_versions_per_interval[$interval]['step'];
734
+        if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
735
+            $nextInterval = -1;
736
+        } else {
737
+            $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
738
+        }
739
+
740
+        $firstVersion = reset($versions);
741
+
742
+        if ($firstVersion === false) {
743
+            return [$toDelete, $size];
744
+        }
745
+
746
+        $firstKey = key($versions);
747
+        $prevTimestamp = $firstVersion['version'];
748
+        $nextVersion = $firstVersion['version'] - $step;
749
+        unset($versions[$firstKey]);
750
+
751
+        foreach ($versions as $key => $version) {
752
+            $newInterval = true;
753
+            while ($newInterval) {
754
+                if ($nextInterval === -1 || $prevTimestamp > $nextInterval) {
755
+                    if ($version['version'] > $nextVersion) {
756
+                        //distance between two version too small, mark to delete
757
+                        $toDelete[$key] = $version['path'] . '.v' . $version['version'];
758
+                        $size += $version['size'];
759
+                        \OC::$server->get(LoggerInterface::class)->info('Mark to expire '. $version['path'] .' next version should be ' . $nextVersion . " or smaller. (prevTimestamp: " . $prevTimestamp . "; step: " . $step, ['app' => 'files_versions']);
760
+                    } else {
761
+                        $nextVersion = $version['version'] - $step;
762
+                        $prevTimestamp = $version['version'];
763
+                    }
764
+                    $newInterval = false; // version checked so we can move to the next one
765
+                } else { // time to move on to the next interval
766
+                    $interval++;
767
+                    $step = Storage::$max_versions_per_interval[$interval]['step'];
768
+                    $nextVersion = $prevTimestamp - $step;
769
+                    if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] === -1) {
770
+                        $nextInterval = -1;
771
+                    } else {
772
+                        $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
773
+                    }
774
+                    $newInterval = true; // we changed the interval -> check same version with new interval
775
+                }
776
+            }
777
+        }
778
+
779
+        return [$toDelete, $size];
780
+    }
781
+
782
+    /**
783
+     * Schedule versions expiration for the given file
784
+     *
785
+     * @param string $uid owner of the file
786
+     * @param string $fileName file/folder for which to schedule expiration
787
+     */
788
+    public static function scheduleExpire($uid, $fileName) {
789
+        // let the admin disable auto expire
790
+        $expiration = self::getExpiration();
791
+        if ($expiration->isEnabled()) {
792
+            $command = new Expire($uid, $fileName);
793
+            /** @var IBus $bus */
794
+            $bus = \OC::$server->get(IBus::class);
795
+            $bus->push($command);
796
+        }
797
+    }
798
+
799
+    /**
800
+     * Expire versions which exceed the quota.
801
+     *
802
+     * This will setup the filesystem for the given user but will not
803
+     * tear it down afterwards.
804
+     *
805
+     * @param string $filename path to file to expire
806
+     * @param string $uid user for which to expire the version
807
+     * @return bool|int|null
808
+     */
809
+    public static function expire($filename, $uid) {
810
+        $expiration = self::getExpiration();
811
+
812
+        /** @var LoggerInterface $logger */
813
+        $logger = \OC::$server->get(LoggerInterface::class);
814
+
815
+        if ($expiration->isEnabled()) {
816
+            // get available disk space for user
817
+            $user = \OC::$server->get(IUserManager::class)->get($uid);
818
+            if (is_null($user)) {
819
+                $logger->error('Backends provided no user object for ' . $uid, ['app' => 'files_versions']);
820
+                throw new \OC\User\NoUserException('Backends provided no user object for ' . $uid);
821
+            }
822
+
823
+            \OC_Util::setupFS($uid);
824
+
825
+            try {
826
+                if (!Filesystem::file_exists($filename)) {
827
+                    return false;
828
+                }
829
+            } catch (StorageNotAvailableException $e) {
830
+                // if we can't check that the file hasn't been deleted we can only assume that it hasn't
831
+                // note that this `StorageNotAvailableException` is about the file the versions originate from,
832
+                // not the storage that the versions are stored on
833
+            }
834
+
835
+            if (empty($filename)) {
836
+                // file maybe renamed or deleted
837
+                return false;
838
+            }
839
+            $versionsFileview = new View('/'.$uid.'/files_versions');
840
+
841
+            $softQuota = true;
842
+            $quota = $user->getQuota();
843
+            if ($quota === null || $quota === 'none') {
844
+                $quota = Filesystem::free_space('/');
845
+                $softQuota = false;
846
+            } else {
847
+                $quota = \OCP\Util::computerFileSize($quota);
848
+            }
849
+
850
+            // make sure that we have the current size of the version history
851
+            $versionsSize = self::getVersionsSize($uid);
852
+
853
+            // calculate available space for version history
854
+            // subtract size of files and current versions size from quota
855
+            if ($quota >= 0) {
856
+                if ($softQuota) {
857
+                    $root = \OC::$server->get(IRootFolder::class);
858
+                    $userFolder = $root->getUserFolder($uid);
859
+                    if (is_null($userFolder)) {
860
+                        $availableSpace = 0;
861
+                    } else {
862
+                        $free = $quota - $userFolder->getSize(false); // remaining free space for user
863
+                        if ($free > 0) {
864
+                            $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $versionsSize; // how much space can be used for versions
865
+                        } else {
866
+                            $availableSpace = $free - $versionsSize;
867
+                        }
868
+                    }
869
+                } else {
870
+                    $availableSpace = $quota;
871
+                }
872
+            } else {
873
+                $availableSpace = PHP_INT_MAX;
874
+            }
875
+
876
+            $allVersions = Storage::getVersions($uid, $filename);
877
+
878
+            $time = time();
879
+            [$toDelete, $sizeOfDeletedVersions] = self::getExpireList($time, $allVersions, $availableSpace <= 0);
880
+
881
+            $availableSpace = $availableSpace + $sizeOfDeletedVersions;
882
+            $versionsSize = $versionsSize - $sizeOfDeletedVersions;
883
+
884
+            // if still not enough free space we rearrange the versions from all files
885
+            if ($availableSpace <= 0) {
886
+                $result = self::getAllVersions($uid);
887
+                $allVersions = $result['all'];
888
+
889
+                foreach ($result['by_file'] as $versions) {
890
+                    [$toDeleteNew, $size] = self::getExpireList($time, $versions, $availableSpace <= 0);
891
+                    $toDelete = array_merge($toDelete, $toDeleteNew);
892
+                    $sizeOfDeletedVersions += $size;
893
+                }
894
+                $availableSpace = $availableSpace + $sizeOfDeletedVersions;
895
+                $versionsSize = $versionsSize - $sizeOfDeletedVersions;
896
+            }
897
+
898
+            foreach ($toDelete as $key => $path) {
899
+                \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
900
+                self::deleteVersion($versionsFileview, $path);
901
+                \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
902
+                unset($allVersions[$key]); // update array with the versions we keep
903
+                $logger->info('Expire: ' . $path, ['app' => 'files_versions']);
904
+            }
905
+
906
+            // Check if enough space is available after versions are rearranged.
907
+            // If not we delete the oldest versions until we meet the size limit for versions,
908
+            // but always keep the two latest versions
909
+            $numOfVersions = count($allVersions) - 2 ;
910
+            $i = 0;
911
+            // sort oldest first and make sure that we start at the first element
912
+            ksort($allVersions);
913
+            reset($allVersions);
914
+            while ($availableSpace < 0 && $i < $numOfVersions) {
915
+                $version = current($allVersions);
916
+                \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
917
+                self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']);
918
+                \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]);
919
+                $logger->info('running out of space! Delete oldest version: ' . $version['path'].'.v'.$version['version'], ['app' => 'files_versions']);
920
+                $versionsSize -= $version['size'];
921
+                $availableSpace += $version['size'];
922
+                next($allVersions);
923
+                $i++;
924
+            }
925
+
926
+            return $versionsSize; // finally return the new size of the version history
927
+        }
928
+
929
+        return false;
930
+    }
931
+
932
+    /**
933
+     * Create recursively missing directories inside of files_versions
934
+     * that match the given path to a file.
935
+     *
936
+     * @param string $filename $path to a file, relative to the user's
937
+     * "files" folder
938
+     * @param View $view view on data/user/
939
+     */
940
+    public static function createMissingDirectories($filename, $view) {
941
+        $dirname = Filesystem::normalizePath(dirname($filename));
942
+        $dirParts = explode('/', $dirname);
943
+        $dir = "/files_versions";
944
+        foreach ($dirParts as $part) {
945
+            $dir = $dir . '/' . $part;
946
+            if (!$view->file_exists($dir)) {
947
+                $view->mkdir($dir);
948
+            }
949
+        }
950
+    }
951
+
952
+    /**
953
+     * Static workaround
954
+     * @return Expiration
955
+     */
956
+    protected static function getExpiration() {
957
+        if (self::$application === null) {
958
+            self::$application = \OC::$server->get(Application::class);
959
+        }
960
+        return self::$application->getContainer()->get(Expiration::class);
961
+    }
962 962
 }
Please login to merge, or discard this patch.
apps/files_versions/lib/Versions/VersionManager.php 1 patch
Indentation   +102 added lines, -102 removed lines patch added patch discarded remove patch
@@ -31,106 +31,106 @@
 block discarded – undo
31 31
 use OCP\IUser;
32 32
 
33 33
 class VersionManager implements IVersionManager, INameableVersionBackend, IDeletableVersionBackend {
34
-	/** @var (IVersionBackend[])[] */
35
-	private $backends = [];
36
-
37
-	public function registerBackend(string $storageType, IVersionBackend $backend) {
38
-		if (!isset($this->backends[$storageType])) {
39
-			$this->backends[$storageType] = [];
40
-		}
41
-		$this->backends[$storageType][] = $backend;
42
-	}
43
-
44
-	/**
45
-	 * @return (IVersionBackend[])[]
46
-	 */
47
-	private function getBackends(): array {
48
-		return $this->backends;
49
-	}
50
-
51
-	/**
52
-	 * @param IStorage $storage
53
-	 * @return IVersionBackend
54
-	 * @throws BackendNotFoundException
55
-	 */
56
-	public function getBackendForStorage(IStorage $storage): IVersionBackend {
57
-		$fullType = get_class($storage);
58
-		$backends = $this->getBackends();
59
-
60
-		$foundType = '';
61
-		$foundBackend = null;
62
-
63
-		foreach ($backends as $type => $backendsForType) {
64
-			if (
65
-				$storage->instanceOfStorage($type) &&
66
-				($foundType === '' || is_subclass_of($type, $foundType))
67
-			) {
68
-				foreach ($backendsForType as $backend) {
69
-					/** @var IVersionBackend $backend */
70
-					if ($backend->useBackendForStorage($storage)) {
71
-						$foundBackend = $backend;
72
-						$foundType = $type;
73
-					}
74
-				}
75
-			}
76
-		}
77
-
78
-		if ($foundType === '' || $foundBackend === null) {
79
-			throw new BackendNotFoundException("Version backend for $fullType not found");
80
-		} else {
81
-			return $foundBackend;
82
-		}
83
-	}
84
-
85
-	public function getVersionsForFile(IUser $user, FileInfo $file): array {
86
-		$backend = $this->getBackendForStorage($file->getStorage());
87
-		return $backend->getVersionsForFile($user, $file);
88
-	}
89
-
90
-	public function createVersion(IUser $user, FileInfo $file) {
91
-		$backend = $this->getBackendForStorage($file->getStorage());
92
-		$backend->createVersion($user, $file);
93
-	}
94
-
95
-	public function rollback(IVersion $version) {
96
-		$backend = $version->getBackend();
97
-		$result = $backend->rollback($version);
98
-		// rollback doesn't have a return type yet and some implementations don't return anything
99
-		if ($result === null || $result === true) {
100
-			\OC_Hook::emit('\OCP\Versions', 'rollback', [
101
-				'path' => $version->getVersionPath(),
102
-				'revision' => $version->getRevisionId(),
103
-				'node' => $version->getSourceFile(),
104
-			]);
105
-		}
106
-		return $result;
107
-	}
108
-
109
-	public function read(IVersion $version) {
110
-		$backend = $version->getBackend();
111
-		return $backend->read($version);
112
-	}
113
-
114
-	public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
115
-		$backend = $this->getBackendForStorage($sourceFile->getStorage());
116
-		return $backend->getVersionFile($user, $sourceFile, $revision);
117
-	}
118
-
119
-	public function useBackendForStorage(IStorage $storage): bool {
120
-		return false;
121
-	}
122
-
123
-	public function setVersionLabel(IVersion $version, string $label): void {
124
-		$backend = $this->getBackendForStorage($version->getSourceFile()->getStorage());
125
-		if ($backend instanceof INameableVersionBackend) {
126
-			$backend->setVersionLabel($version, $label);
127
-		}
128
-	}
129
-
130
-	public function deleteVersion(IVersion $version): void {
131
-		$backend = $this->getBackendForStorage($version->getSourceFile()->getStorage());
132
-		if ($backend instanceof IDeletableVersionBackend) {
133
-			$backend->deleteVersion($version);
134
-		}
135
-	}
34
+    /** @var (IVersionBackend[])[] */
35
+    private $backends = [];
36
+
37
+    public function registerBackend(string $storageType, IVersionBackend $backend) {
38
+        if (!isset($this->backends[$storageType])) {
39
+            $this->backends[$storageType] = [];
40
+        }
41
+        $this->backends[$storageType][] = $backend;
42
+    }
43
+
44
+    /**
45
+     * @return (IVersionBackend[])[]
46
+     */
47
+    private function getBackends(): array {
48
+        return $this->backends;
49
+    }
50
+
51
+    /**
52
+     * @param IStorage $storage
53
+     * @return IVersionBackend
54
+     * @throws BackendNotFoundException
55
+     */
56
+    public function getBackendForStorage(IStorage $storage): IVersionBackend {
57
+        $fullType = get_class($storage);
58
+        $backends = $this->getBackends();
59
+
60
+        $foundType = '';
61
+        $foundBackend = null;
62
+
63
+        foreach ($backends as $type => $backendsForType) {
64
+            if (
65
+                $storage->instanceOfStorage($type) &&
66
+                ($foundType === '' || is_subclass_of($type, $foundType))
67
+            ) {
68
+                foreach ($backendsForType as $backend) {
69
+                    /** @var IVersionBackend $backend */
70
+                    if ($backend->useBackendForStorage($storage)) {
71
+                        $foundBackend = $backend;
72
+                        $foundType = $type;
73
+                    }
74
+                }
75
+            }
76
+        }
77
+
78
+        if ($foundType === '' || $foundBackend === null) {
79
+            throw new BackendNotFoundException("Version backend for $fullType not found");
80
+        } else {
81
+            return $foundBackend;
82
+        }
83
+    }
84
+
85
+    public function getVersionsForFile(IUser $user, FileInfo $file): array {
86
+        $backend = $this->getBackendForStorage($file->getStorage());
87
+        return $backend->getVersionsForFile($user, $file);
88
+    }
89
+
90
+    public function createVersion(IUser $user, FileInfo $file) {
91
+        $backend = $this->getBackendForStorage($file->getStorage());
92
+        $backend->createVersion($user, $file);
93
+    }
94
+
95
+    public function rollback(IVersion $version) {
96
+        $backend = $version->getBackend();
97
+        $result = $backend->rollback($version);
98
+        // rollback doesn't have a return type yet and some implementations don't return anything
99
+        if ($result === null || $result === true) {
100
+            \OC_Hook::emit('\OCP\Versions', 'rollback', [
101
+                'path' => $version->getVersionPath(),
102
+                'revision' => $version->getRevisionId(),
103
+                'node' => $version->getSourceFile(),
104
+            ]);
105
+        }
106
+        return $result;
107
+    }
108
+
109
+    public function read(IVersion $version) {
110
+        $backend = $version->getBackend();
111
+        return $backend->read($version);
112
+    }
113
+
114
+    public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
115
+        $backend = $this->getBackendForStorage($sourceFile->getStorage());
116
+        return $backend->getVersionFile($user, $sourceFile, $revision);
117
+    }
118
+
119
+    public function useBackendForStorage(IStorage $storage): bool {
120
+        return false;
121
+    }
122
+
123
+    public function setVersionLabel(IVersion $version, string $label): void {
124
+        $backend = $this->getBackendForStorage($version->getSourceFile()->getStorage());
125
+        if ($backend instanceof INameableVersionBackend) {
126
+            $backend->setVersionLabel($version, $label);
127
+        }
128
+    }
129
+
130
+    public function deleteVersion(IVersion $version): void {
131
+        $backend = $this->getBackendForStorage($version->getSourceFile()->getStorage());
132
+        if ($backend instanceof IDeletableVersionBackend) {
133
+            $backend->deleteVersion($version);
134
+        }
135
+    }
136 136
 }
Please login to merge, or discard this patch.
apps/files_versions/lib/Versions/LegacyVersionsBackend.php 2 patches
Indentation   +155 added lines, -155 removed lines patch added patch discarded remove patch
@@ -43,159 +43,159 @@
 block discarded – undo
43 43
 use OCP\IUserManager;
44 44
 
45 45
 class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend {
46
-	private IRootFolder $rootFolder;
47
-	private IUserManager $userManager;
48
-	private VersionsMapper $versionsMapper;
49
-	private IMimeTypeLoader $mimeTypeLoader;
50
-
51
-	public function __construct(
52
-		IRootFolder $rootFolder,
53
-		IUserManager $userManager,
54
-		VersionsMapper $versionsMapper,
55
-		IMimeTypeLoader $mimeTypeLoader
56
-	) {
57
-		$this->rootFolder = $rootFolder;
58
-		$this->userManager = $userManager;
59
-		$this->versionsMapper = $versionsMapper;
60
-		$this->mimeTypeLoader = $mimeTypeLoader;
61
-	}
62
-
63
-	public function useBackendForStorage(IStorage $storage): bool {
64
-		return true;
65
-	}
66
-
67
-	public function getVersionsForFile(IUser $user, FileInfo $file): array {
68
-		$storage = $file->getStorage();
69
-		if ($storage->instanceOfStorage(SharedStorage::class)) {
70
-			$owner = $storage->getOwner('');
71
-			$user = $this->userManager->get($owner);
72
-
73
-			$userFolder = $this->rootFolder->getUserFolder($user->getUID());
74
-			$nodes = $userFolder->getById($file->getId());
75
-			$file = array_pop($nodes);
76
-
77
-			if (!$file) {
78
-				throw new NotFoundException("version file not found for share owner");
79
-			}
80
-		}
81
-
82
-		$versions = $this->getVersionsForFileFromDB($file, $user);
83
-
84
-		if (count($versions) > 0) {
85
-			return $versions;
86
-		}
87
-
88
-		// Insert the entry in the DB for the current version.
89
-		$versionEntity = new VersionEntity();
90
-		$versionEntity->setFileId($file->getId());
91
-		$versionEntity->setTimestamp($file->getMTime());
92
-		$versionEntity->setSize($file->getSize());
93
-		$versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
94
-		$versionEntity->setMetadata([]);
95
-		$this->versionsMapper->insert($versionEntity);
96
-
97
-		// Insert entries in the DB for existing versions.
98
-		$versionsOnFS = Storage::getVersions($user->getUID(), $userFolder->getRelativePath($file->getPath()));
99
-		foreach ($versionsOnFS as $version) {
100
-			$versionEntity = new VersionEntity();
101
-			$versionEntity->setFileId($file->getId());
102
-			$versionEntity->setTimestamp((int)$version['version']);
103
-			$versionEntity->setSize((int)$version['size']);
104
-			$versionEntity->setMimetype($this->mimeTypeLoader->getId($version['mimetype']));
105
-			$versionEntity->setMetadata([]);
106
-			$this->versionsMapper->insert($versionEntity);
107
-		}
108
-
109
-		return $this->getVersionsForFileFromDB($file, $user);
110
-	}
111
-
112
-	/**
113
-	 * @return IVersion[]
114
-	 */
115
-	private function getVersionsForFileFromDB(FileInfo $file, IUser $user): array {
116
-		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
117
-
118
-		return array_map(
119
-			fn (VersionEntity $versionEntity) => new Version(
120
-				$versionEntity->getTimestamp(),
121
-				$versionEntity->getTimestamp(),
122
-				$file->getName(),
123
-				$versionEntity->getSize(),
124
-				$this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()),
125
-				$userFolder->getRelativePath($file->getPath()),
126
-				$file,
127
-				$this,
128
-				$user,
129
-				$versionEntity->getLabel(),
130
-			),
131
-			$this->versionsMapper->findAllVersionsForFileId($file->getId())
132
-		);
133
-	}
134
-
135
-	public function createVersion(IUser $user, FileInfo $file) {
136
-		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
137
-		$relativePath = $userFolder->getRelativePath($file->getPath());
138
-		$userView = new View('/' . $user->getUID());
139
-		// create all parent folders
140
-		Storage::createMissingDirectories($relativePath, $userView);
141
-
142
-		Storage::scheduleExpire($user->getUID(), $relativePath);
143
-
144
-		// store a new version of a file
145
-		$userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime());
146
-		// ensure the file is scanned
147
-		$userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime());
148
-	}
149
-
150
-	public function rollback(IVersion $version) {
151
-		return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser());
152
-	}
153
-
154
-	private function getVersionFolder(IUser $user): Folder {
155
-		$userRoot = $this->rootFolder->getUserFolder($user->getUID())
156
-			->getParent();
157
-		try {
158
-			/** @var Folder $folder */
159
-			$folder = $userRoot->get('files_versions');
160
-			return $folder;
161
-		} catch (NotFoundException $e) {
162
-			return $userRoot->newFolder('files_versions');
163
-		}
164
-	}
165
-
166
-	public function read(IVersion $version) {
167
-		$versions = $this->getVersionFolder($version->getUser());
168
-		/** @var File $file */
169
-		$file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId());
170
-		return $file->fopen('r');
171
-	}
172
-
173
-	public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
174
-		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
175
-		$versionFolder = $this->getVersionFolder($user);
176
-		/** @var File $file */
177
-		$file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision);
178
-		return $file;
179
-	}
180
-
181
-	public function setVersionLabel(IVersion $version, string $label): void {
182
-		$versionEntity = $this->versionsMapper->findVersionForFileId(
183
-			$version->getSourceFile()->getId(),
184
-			$version->getTimestamp(),
185
-		);
186
-		if (trim($label) === '') {
187
-			$label = null;
188
-		}
189
-		$versionEntity->setLabel($label ?? '');
190
-		$this->versionsMapper->update($versionEntity);
191
-	}
192
-
193
-	public function deleteVersion(IVersion $version): void {
194
-		Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId());
195
-		$versionEntity = $this->versionsMapper->findVersionForFileId(
196
-			$version->getSourceFile()->getId(),
197
-			$version->getTimestamp(),
198
-		);
199
-		$this->versionsMapper->delete($versionEntity);
200
-	}
46
+    private IRootFolder $rootFolder;
47
+    private IUserManager $userManager;
48
+    private VersionsMapper $versionsMapper;
49
+    private IMimeTypeLoader $mimeTypeLoader;
50
+
51
+    public function __construct(
52
+        IRootFolder $rootFolder,
53
+        IUserManager $userManager,
54
+        VersionsMapper $versionsMapper,
55
+        IMimeTypeLoader $mimeTypeLoader
56
+    ) {
57
+        $this->rootFolder = $rootFolder;
58
+        $this->userManager = $userManager;
59
+        $this->versionsMapper = $versionsMapper;
60
+        $this->mimeTypeLoader = $mimeTypeLoader;
61
+    }
62
+
63
+    public function useBackendForStorage(IStorage $storage): bool {
64
+        return true;
65
+    }
66
+
67
+    public function getVersionsForFile(IUser $user, FileInfo $file): array {
68
+        $storage = $file->getStorage();
69
+        if ($storage->instanceOfStorage(SharedStorage::class)) {
70
+            $owner = $storage->getOwner('');
71
+            $user = $this->userManager->get($owner);
72
+
73
+            $userFolder = $this->rootFolder->getUserFolder($user->getUID());
74
+            $nodes = $userFolder->getById($file->getId());
75
+            $file = array_pop($nodes);
76
+
77
+            if (!$file) {
78
+                throw new NotFoundException("version file not found for share owner");
79
+            }
80
+        }
81
+
82
+        $versions = $this->getVersionsForFileFromDB($file, $user);
83
+
84
+        if (count($versions) > 0) {
85
+            return $versions;
86
+        }
87
+
88
+        // Insert the entry in the DB for the current version.
89
+        $versionEntity = new VersionEntity();
90
+        $versionEntity->setFileId($file->getId());
91
+        $versionEntity->setTimestamp($file->getMTime());
92
+        $versionEntity->setSize($file->getSize());
93
+        $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype()));
94
+        $versionEntity->setMetadata([]);
95
+        $this->versionsMapper->insert($versionEntity);
96
+
97
+        // Insert entries in the DB for existing versions.
98
+        $versionsOnFS = Storage::getVersions($user->getUID(), $userFolder->getRelativePath($file->getPath()));
99
+        foreach ($versionsOnFS as $version) {
100
+            $versionEntity = new VersionEntity();
101
+            $versionEntity->setFileId($file->getId());
102
+            $versionEntity->setTimestamp((int)$version['version']);
103
+            $versionEntity->setSize((int)$version['size']);
104
+            $versionEntity->setMimetype($this->mimeTypeLoader->getId($version['mimetype']));
105
+            $versionEntity->setMetadata([]);
106
+            $this->versionsMapper->insert($versionEntity);
107
+        }
108
+
109
+        return $this->getVersionsForFileFromDB($file, $user);
110
+    }
111
+
112
+    /**
113
+     * @return IVersion[]
114
+     */
115
+    private function getVersionsForFileFromDB(FileInfo $file, IUser $user): array {
116
+        $userFolder = $this->rootFolder->getUserFolder($user->getUID());
117
+
118
+        return array_map(
119
+            fn (VersionEntity $versionEntity) => new Version(
120
+                $versionEntity->getTimestamp(),
121
+                $versionEntity->getTimestamp(),
122
+                $file->getName(),
123
+                $versionEntity->getSize(),
124
+                $this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()),
125
+                $userFolder->getRelativePath($file->getPath()),
126
+                $file,
127
+                $this,
128
+                $user,
129
+                $versionEntity->getLabel(),
130
+            ),
131
+            $this->versionsMapper->findAllVersionsForFileId($file->getId())
132
+        );
133
+    }
134
+
135
+    public function createVersion(IUser $user, FileInfo $file) {
136
+        $userFolder = $this->rootFolder->getUserFolder($user->getUID());
137
+        $relativePath = $userFolder->getRelativePath($file->getPath());
138
+        $userView = new View('/' . $user->getUID());
139
+        // create all parent folders
140
+        Storage::createMissingDirectories($relativePath, $userView);
141
+
142
+        Storage::scheduleExpire($user->getUID(), $relativePath);
143
+
144
+        // store a new version of a file
145
+        $userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime());
146
+        // ensure the file is scanned
147
+        $userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime());
148
+    }
149
+
150
+    public function rollback(IVersion $version) {
151
+        return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser());
152
+    }
153
+
154
+    private function getVersionFolder(IUser $user): Folder {
155
+        $userRoot = $this->rootFolder->getUserFolder($user->getUID())
156
+            ->getParent();
157
+        try {
158
+            /** @var Folder $folder */
159
+            $folder = $userRoot->get('files_versions');
160
+            return $folder;
161
+        } catch (NotFoundException $e) {
162
+            return $userRoot->newFolder('files_versions');
163
+        }
164
+    }
165
+
166
+    public function read(IVersion $version) {
167
+        $versions = $this->getVersionFolder($version->getUser());
168
+        /** @var File $file */
169
+        $file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId());
170
+        return $file->fopen('r');
171
+    }
172
+
173
+    public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File {
174
+        $userFolder = $this->rootFolder->getUserFolder($user->getUID());
175
+        $versionFolder = $this->getVersionFolder($user);
176
+        /** @var File $file */
177
+        $file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision);
178
+        return $file;
179
+    }
180
+
181
+    public function setVersionLabel(IVersion $version, string $label): void {
182
+        $versionEntity = $this->versionsMapper->findVersionForFileId(
183
+            $version->getSourceFile()->getId(),
184
+            $version->getTimestamp(),
185
+        );
186
+        if (trim($label) === '') {
187
+            $label = null;
188
+        }
189
+        $versionEntity->setLabel($label ?? '');
190
+        $this->versionsMapper->update($versionEntity);
191
+    }
192
+
193
+    public function deleteVersion(IVersion $version): void {
194
+        Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId());
195
+        $versionEntity = $this->versionsMapper->findVersionForFileId(
196
+            $version->getSourceFile()->getId(),
197
+            $version->getTimestamp(),
198
+        );
199
+        $this->versionsMapper->delete($versionEntity);
200
+    }
201 201
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -99,8 +99,8 @@  discard block
 block discarded – undo
99 99
 		foreach ($versionsOnFS as $version) {
100 100
 			$versionEntity = new VersionEntity();
101 101
 			$versionEntity->setFileId($file->getId());
102
-			$versionEntity->setTimestamp((int)$version['version']);
103
-			$versionEntity->setSize((int)$version['size']);
102
+			$versionEntity->setTimestamp((int) $version['version']);
103
+			$versionEntity->setSize((int) $version['size']);
104 104
 			$versionEntity->setMimetype($this->mimeTypeLoader->getId($version['mimetype']));
105 105
 			$versionEntity->setMetadata([]);
106 106
 			$this->versionsMapper->insert($versionEntity);
@@ -135,16 +135,16 @@  discard block
 block discarded – undo
135 135
 	public function createVersion(IUser $user, FileInfo $file) {
136 136
 		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
137 137
 		$relativePath = $userFolder->getRelativePath($file->getPath());
138
-		$userView = new View('/' . $user->getUID());
138
+		$userView = new View('/'.$user->getUID());
139 139
 		// create all parent folders
140 140
 		Storage::createMissingDirectories($relativePath, $userView);
141 141
 
142 142
 		Storage::scheduleExpire($user->getUID(), $relativePath);
143 143
 
144 144
 		// store a new version of a file
145
-		$userView->copy('files/' . $relativePath, 'files_versions/' . $relativePath . '.v' . $file->getMtime());
145
+		$userView->copy('files/'.$relativePath, 'files_versions/'.$relativePath.'.v'.$file->getMtime());
146 146
 		// ensure the file is scanned
147
-		$userView->getFileInfo('files_versions/' . $relativePath . '.v' . $file->getMtime());
147
+		$userView->getFileInfo('files_versions/'.$relativePath.'.v'.$file->getMtime());
148 148
 	}
149 149
 
150 150
 	public function rollback(IVersion $version) {
@@ -166,7 +166,7 @@  discard block
 block discarded – undo
166 166
 	public function read(IVersion $version) {
167 167
 		$versions = $this->getVersionFolder($version->getUser());
168 168
 		/** @var File $file */
169
-		$file = $versions->get($version->getVersionPath() . '.v' . $version->getRevisionId());
169
+		$file = $versions->get($version->getVersionPath().'.v'.$version->getRevisionId());
170 170
 		return $file->fopen('r');
171 171
 	}
172 172
 
@@ -174,7 +174,7 @@  discard block
 block discarded – undo
174 174
 		$userFolder = $this->rootFolder->getUserFolder($user->getUID());
175 175
 		$versionFolder = $this->getVersionFolder($user);
176 176
 		/** @var File $file */
177
-		$file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision);
177
+		$file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()).'.v'.$revision);
178 178
 		return $file;
179 179
 	}
180 180
 
Please login to merge, or discard this patch.