Completed
Pull Request — stable8 (#25330)
by
unknown
16:49
created

Storage::rollback()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 25
c 1
b 0
f 0
nc 9
nop 2
dl 0
loc 46
rs 5.5555
1
<?php
2
/**
3
 * Copyright (c) 2012 Frank Karlitschek <[email protected]>
4
 *               2013 Bjoern Schiessle <[email protected]>
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later.
7
 * See the COPYING-README file.
8
 */
9
10
/**
11
 * Versions
12
 *
13
 * A class to handle the versioning of files.
14
 */
15
16
namespace OCA\Files_Versions;
17
18
class Storage {
19
20
	const DEFAULTENABLED=true;
21
	const DEFAULTMAXSIZE=50; // unit: percentage; 50% of available disk space/quota
22
	const VERSIONS_ROOT = 'files_versions/';
23
24
	// files for which we can remove the versions after the delete operation was successful
25
	private static $deletedFiles = array();
26
27
	private static $sourcePathAndUser = array();
28
29
	private static $max_versions_per_interval = array(
30
		//first 10sec, one version every 2sec
31
		1 => array('intervalEndsAfter' => 10,      'step' => 2),
32
		//next minute, one version every 10sec
33
		2 => array('intervalEndsAfter' => 60,      'step' => 10),
34
		//next hour, one version every minute
35
		3 => array('intervalEndsAfter' => 3600,    'step' => 60),
36
		//next 24h, one version every hour
37
		4 => array('intervalEndsAfter' => 86400,   'step' => 3600),
38
		//next 30days, one version per day
39
		5 => array('intervalEndsAfter' => 2592000, 'step' => 86400),
40
		//until the end one version per week
41
		6 => array('intervalEndsAfter' => -1,      'step' => 604800),
42
	);
43
44 View Code Duplication
	public static function getUidAndFilename($filename) {
45
		$uid = \OC\Files\Filesystem::getOwner($filename);
46
		\OC\Files\Filesystem::initMountPoints($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by \OC\Files\Filesystem::getOwner($filename) on line 45 can also be of type false or null; however, OC\Files\Filesystem::initMountPoints() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
47
		if ( $uid != \OCP\User::getUser() ) {
48
			$info = \OC\Files\Filesystem::getFileInfo($filename);
49
			$ownerView = new \OC\Files\View('/'.$uid.'/files');
50
			$filename = $ownerView->getPath($info['fileid']);
51
		}
52
		return array($uid, $filename);
53
	}
54
55
	/**
56
	 * Remember the owner and the owner path of the source file
57
	 *
58
	 * @param string $source source path
59
	 */
60
	public static function setSourcePathAndUser($source) {
61
		list($uid, $path) = self::getUidAndFilename($source);
62
		self::$sourcePathAndUser[$source] = array('uid' => $uid, 'path' => $path);
63
	}
64
65
	/**
66
	 * Gets the owner and the owner path from the source path
67
	 *
68
	 * @param string $source source path
69
	 * @return array with user id and path
70
	 */
71
	public static function getSourcePathAndUser($source) {
72
73
		if (isset(self::$sourcePathAndUser[$source])) {
74
			$uid = self::$sourcePathAndUser[$source]['uid'];
75
			$path = self::$sourcePathAndUser[$source]['path'];
76
			unset(self::$sourcePathAndUser[$source]);
77
		} else {
78
			$uid = $path = false;
79
		}
80
		return array($uid, $path);
81
	}
82
83
	/**
84
	 * get current size of all versions from a given user
85
	 *
86
	 * @param string $user user who owns the versions
87
	 * @return int versions size
88
	 */
89
	private static function getVersionsSize($user) {
90
		$view = new \OC\Files\View('/' . $user);
91
		$fileInfo = $view->getFileInfo('/files_versions');
92
		return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
93
	}
94
95
	/**
96
	 * store a new version of a file.
97
	 */
98
	public static function store($filename) {
99
		if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
100
101
			// if the file gets streamed we need to remove the .part extension
102
			// to get the right target
103
			$ext = pathinfo($filename, PATHINFO_EXTENSION);
104
			if ($ext === 'part') {
105
				$filename = substr($filename, 0, strlen($filename)-5);
106
			}
107
108
			list($uid, $filename) = self::getUidAndFilename($filename);
109
110
			$files_view = new \OC\Files\View('/'.$uid .'/files');
111
			$users_view = new \OC\Files\View('/'.$uid);
112
113
			// check if filename is a directory
114
			if($files_view->is_dir($filename)) {
115
				return false;
116
			}
117
118
			// we should have a source file to work with, and the file shouldn't
119
			// be empty
120
			$fileExists = $files_view->file_exists($filename);
121
			if (!($fileExists && $files_view->filesize($filename) > 0)) {
122
				return false;
123
			}
124
125
			// create all parent folders
126
			self::createMissingDirectories($filename, $users_view);
127
128
			$versionsSize = self::getVersionsSize($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by self::getUidAndFilename($filename) on line 108 can also be of type false or null; however, OCA\Files_Versions\Storage::getVersionsSize() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
129
130
			// assumption: we need filesize($filename) for the new version +
131
			// some more free space for the modified file which might be
132
			// 1.5 times as large as the current version -> 2.5
133
			$neededSpace = $files_view->filesize($filename) * 2.5;
134
135
			self::expire($filename, $versionsSize, $neededSpace);
136
137
			// disable proxy to prevent multiple fopen calls
138
			$proxyStatus = \OC_FileProxy::$enabled;
139
			\OC_FileProxy::$enabled = false;
140
141
			// store a new version of a file
142
			$mtime = $users_view->filemtime('files/' . $filename);
143
			$users_view->copy('files/' . $filename, 'files_versions/' . $filename . '.v' . $mtime);
144
			// call getFileInfo to enforce a file cache entry for the new version
145
			$users_view->getFileInfo('files_versions/' . $filename . '.v' . $mtime);
146
147
			// reset proxy state
148
			\OC_FileProxy::$enabled = $proxyStatus;
149
		}
150
	}
151
152
153
	/**
154
	 * mark file as deleted so that we can remove the versions if the file is gone
155
	 * @param string $path
156
	 */
157
	public static function markDeletedFile($path) {
158
		list($uid, $filename) = self::getUidAndFilename($path);
159
		self::$deletedFiles[$path] = array(
160
			'uid' => $uid,
161
			'filename' => $filename);
162
	}
163
164
	/**
165
	 * delete the version from the storage and cache
166
	 *
167
	 * @param \OC\Files\View $view
168
	 * @param string $path
169
	 */
170
	protected static function deleteVersion($view, $path) {
171
		$view->unlink($path);
172
		/**
173
		 * @var \OC\Files\Storage\Storage $storage
174
		 * @var string $internalPath
175
		 */
176
		list($storage, $internalPath) = $view->resolvePath($path);
177
		$cache = $storage->getCache($internalPath);
178
		$cache->remove($internalPath);
179
	}
180
181
	/**
182
	 * Delete versions of a file
183
	 */
184
	public static function delete($path) {
185
186
		$deletedFile = self::$deletedFiles[$path];
187
		$uid = $deletedFile['uid'];
188
		$filename = $deletedFile['filename'];
189
190
		if (!\OC\Files\Filesystem::file_exists($path)) {
191
192
			$view = new \OC\Files\View('/' . $uid . '/files_versions');
193
194
			$versions = self::getVersions($uid, $filename);
195
			if (!empty($versions)) {
196
				foreach ($versions as $v) {
197
					\OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $path . $v['version']));
198
					self::deleteVersion($view, $filename . '.v' . $v['version']);
199
					\OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $path . $v['version']));
200
				}
201
			}
202
		}
203
		unset(self::$deletedFiles[$path]);
204
	}
205
206
	/**
207
	 * Rename or copy versions of a file of the given paths
208
	 *
209
	 * @param string $sourcePath source path of the file to move, relative to
210
	 * the currently logged in user's "files" folder
211
	 * @param string $targetPath target path of the file to move, relative to
212
	 * the currently logged in user's "files" folder
213
	 * @param string $operation can be 'copy' or 'rename'
214
	 */
215
	public static function renameOrCopy($sourcePath, $targetPath, $operation) {
216
		list($sourceOwner, $sourcePath) = self::getSourcePathAndUser($sourcePath);
217
218
		// it was a upload of a existing file if no old path exists
219
		// in this case the pre-hook already called the store method and we can
220
		// stop here
221
		if ($sourcePath === false) {
222
			return true;
223
		}
224
225
		list($targetOwner, $targetPath) = self::getUidAndFilename($targetPath);
226
227
		$sourcePath = ltrim($sourcePath, '/');
228
		$targetPath = ltrim($targetPath, '/');
229
230
		$rootView = new \OC\Files\View('');
231
232
		// did we move a directory ?
233
		if ($rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
234
			// does the directory exists for versions too ?
235
			if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) {
236
				// create missing dirs if necessary
237
				self::createMissingDirectories($targetPath, new \OC\Files\View('/'. $targetOwner));
238
239
				// move the directory containing the versions
240
				$rootView->$operation(
241
					'/' . $sourceOwner . '/files_versions/' . $sourcePath,
242
					'/' . $targetOwner . '/files_versions/' . $targetPath
243
				);
244
			}
245
		} else if ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) {
246
			// create missing dirs if necessary
247
			self::createMissingDirectories($targetPath, new \OC\Files\View('/'. $targetOwner));
248
249
			foreach ($versions as $v) {
250
				// move each version one by one to the target directory
251
				$rootView->$operation(
252
					'/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'],
253
					'/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version']
254
				);
255
			}
256
		}
257
258
		// if we moved versions directly for a file, schedule expiration check for that file
259
		if (!$rootView->is_dir('/' . $targetOwner . '/files/' . $targetPath)) {
260
			self::expire($targetPath);
261
		}
262
263
	}
264
265
	/**
266
	 * Rollback to an old version of a file.
267
	 *
268
	 * @param string $file file name
269
	 * @param int $revision revision timestamp
270
	 */
271
	public static function rollback($file, $revision) {
272
273
		if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
274
			// add expected leading slash
275
			$file = '/' . ltrim($file, '/');
276
			list($uid, $filename) = self::getUidAndFilename($file);
277
			if ($uid === null || trim($filename, '/') === '') {
278
				return false;
279
			}
280
			$users_view = new \OC\Files\View('/'.$uid);
281
			$files_view = new \OC\Files\View('/'.\OCP\User::getUser().'/files');
282
283
			if (!$files_view->isUpdatable($filename)) {
284
				return false;
285
			}
286
287
			$versionCreated = false;
288
289
			//first create a new version
290
			$version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename);
291
			if ( !$users_view->file_exists($version)) {
292
293
				// disable proxy to prevent multiple fopen calls
294
				$proxyStatus = \OC_FileProxy::$enabled;
295
				\OC_FileProxy::$enabled = false;
296
297
				$users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
298
299
				// reset proxy state
300
				\OC_FileProxy::$enabled = $proxyStatus;
301
302
				$versionCreated = true;
303
			}
304
305
			// rollback
306
			if (self::copyFileContents($users_view, 'files_versions' . $filename . '.v' . $revision, 'files' . $filename)) {
307
				$files_view->touch($file, $revision);
308
				Storage::expire($file);
309
				return true;
310
			} else if ($versionCreated) {
311
				self::deleteVersion($users_view, $version);
312
			}
313
		}
314
		return false;
315
316
	}
317
318
	/**
319
	 * Stream copy file contents from $path1 to $path2
320
	 *
321
	 * @param \OC\Files\View $view view to use for copying
322
	 * @param string $path1 source file to copy
323
	 * @param string $path2 target file
324
	 *
325
	 * @return bool true for success, false otherwise
326
	 */
327
	private static function copyFileContents($view, $path1, $path2) {
328
		list($storage1, $internalPath1) = $view->resolvePath($path1);
329
		list($storage2, $internalPath2) = $view->resolvePath($path2);
330
331
		if ($storage1 === $storage2) {
332
			return $storage1->rename($internalPath1, $internalPath2);
333
		}
334
		$source = $storage1->fopen($internalPath1, 'r');
335
		$target = $storage2->fopen($internalPath2, 'w');
336
		// FIXME: might need to use part file to avoid concurrent writes
337
		// (this would be an issue anyway when renaming/restoring cross-storage)
338
		list(, $result) = \OC_Helper::streamCopy($source, $target);
339
		fclose($source);
340
		fclose($target);
341
342
		if ($result !== false) {
343
			$storage1->unlink($internalPath1);
344
		}
345
346
		return ($result !== false);
347
	}
348
349
	/**
350
	 * get a list of all available versions of a file in descending chronological order
351
	 * @param string $uid user id from the owner of the file
352
	 * @param string $filename file to find versions of, relative to the user files dir
353
	 * @param string $userFullPath
354
	 * @return array versions newest version first
355
	 */
356
	public static function getVersions($uid, $filename, $userFullPath = '') {
357
		$versions = array();
358
		// fetch for old versions
359
		$view = new \OC\Files\View('/' . $uid . '/');
360
361
		$pathinfo = pathinfo($filename);
362
		$versionedFile = $pathinfo['basename'];
363
364
		$dir = \OC\Files\Filesystem::normalizePath(self::VERSIONS_ROOT . '/' . $pathinfo['dirname']);
365
366
		$dirContent = false;
367
		if ($view->is_dir($dir)) {
368
			$dirContent = $view->opendir($dir);
369
		}
370
371
		if ($dirContent === false) {
372
			return $versions;
373
		}
374
375
		if (is_resource($dirContent)) {
376
			while (($entryName = readdir($dirContent)) !== false) {
377
				if (!\OC\Files\Filesystem::isIgnoredDir($entryName)) {
378
					$pathparts = pathinfo($entryName);
379
					$filename = $pathparts['filename'];
380
					if ($filename === $versionedFile) {
381
						$pathparts = pathinfo($entryName);
382
						$timestamp = substr($pathparts['extension'], 1);
383
						$filename = $pathparts['filename'];
384
						$key = $timestamp . '#' . $filename;
385
						$versions[$key]['version'] = $timestamp;
386
						$versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp($timestamp);
387
						if (empty($userFullPath)) {
388
							$versions[$key]['preview'] = '';
389
						} else {
390
							$versions[$key]['preview'] = \OCP\Util::linkToRoute('core_ajax_versions_preview', array('file' => $userFullPath, 'version' => $timestamp));
391
						}
392
						$versions[$key]['path'] = \OC\Files\Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename);
393
						$versions[$key]['name'] = $versionedFile;
394
						$versions[$key]['size'] = $view->filesize($dir . '/' . $entryName);
395
					}
396
				}
397
			}
398
			closedir($dirContent);
399
		}
400
401
		// sort with newest version first
402
		krsort($versions);
403
404
		return $versions;
405
	}
406
407
	/**
408
	 * translate a timestamp into a string like "5 days ago"
409
	 * @param int $timestamp
410
	 * @return string for example "5 days ago"
411
	 */
412
	private static function getHumanReadableTimestamp($timestamp) {
413
414
		$diff = time() - $timestamp;
415
416
		if ($diff < 60) { // first minute
417
			return  $diff . " seconds ago";
418
		} elseif ($diff < 3600) { //first hour
419
			return round($diff / 60) . " minutes ago";
420
		} elseif ($diff < 86400) { // first day
421
			return round($diff / 3600) . " hours ago";
422
		} elseif ($diff < 604800) { //first week
423
			return round($diff / 86400) . " days ago";
424
		} elseif ($diff < 2419200) { //first month
425
			return round($diff / 604800) . " weeks ago";
426
		} elseif ($diff < 29030400) { // first year
427
			return round($diff / 2419200) . " months ago";
428
		} else {
429
			return round($diff / 29030400) . " years ago";
430
		}
431
432
	}
433
434
	/**
435
	 * returns all stored file versions from a given user
436
	 * @param string $uid id of the user
437
	 * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
438
	 */
439
	private static function getAllVersions($uid) {
440
		$view = new \OC\Files\View('/' . $uid . '/');
441
		$dirs = array(self::VERSIONS_ROOT);
442
		$versions = array();
443
444
		while (!empty($dirs)) {
445
			$dir = array_pop($dirs);
446
			$files = $view->getDirectoryContent($dir);
447
448
			foreach ($files as $file) {
449
				if ($file['type'] === 'dir') {
450
					array_push($dirs, $file['path']);
451
				} else {
452
					$versionsBegin = strrpos($file['path'], '.v');
453
					$relPathStart = strlen(self::VERSIONS_ROOT);
454
					$version = substr($file['path'], $versionsBegin + 2);
455
					$relpath = substr($file['path'], $relPathStart, $versionsBegin - $relPathStart);
456
					$key = $version . '#' . $relpath;
457
					$versions[$key] = array('path' => $relpath, 'timestamp' => $version);
458
				}
459
			}
460
		}
461
462
		// newest version first
463
		krsort($versions);
464
465
		$result = array();
466
467
		foreach ($versions as $key => $value) {
468
			$size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']);
469
			$filename = $value['path'];
470
471
			$result['all'][$key]['version'] = $value['timestamp'];
472
			$result['all'][$key]['path'] = $filename;
473
			$result['all'][$key]['size'] = $size;
474
475
			$result['by_file'][$filename][$key]['version'] = $value['timestamp'];
476
			$result['by_file'][$filename][$key]['path'] = $filename;
477
			$result['by_file'][$filename][$key]['size'] = $size;
478
		}
479
480
		return $result;
481
	}
482
483
	/**
484
	 * get list of files we want to expire
485
	 * @param array $versions list of versions
486
	 * @param integer $time
487
	 * @return array containing the list of to deleted versions and the size of them
488
	 */
489
	protected static function getExpireList($time, $versions) {
490
491
		$size = 0;
492
		$toDelete = array();  // versions we want to delete
493
494
		$interval = 1;
495
		$step = Storage::$max_versions_per_interval[$interval]['step'];
496 View Code Duplication
		if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
497
			$nextInterval = -1;
498
		} else {
499
			$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
500
		}
501
502
		$firstVersion = reset($versions);
503
		$firstKey = key($versions);
504
		$prevTimestamp = $firstVersion['version'];
505
		$nextVersion = $firstVersion['version'] - $step;
506
		unset($versions[$firstKey]);
507
508
		foreach ($versions as $key => $version) {
509
			$newInterval = true;
510
			while ($newInterval) {
511
				if ($nextInterval == -1 || $prevTimestamp > $nextInterval) {
512
					if ($version['version'] > $nextVersion) {
513
						//distance between two version too small, mark to delete
514
						$toDelete[$key] = $version['path'] . '.v' . $version['version'];
515
						$size += $version['size'];
516
						\OCP\Util::writeLog('files_versions', 'Mark to expire '. $version['path'] .' next version should be ' . $nextVersion . " or smaller. (prevTimestamp: " . $prevTimestamp . "; step: " . $step, \OCP\Util::DEBUG);
517
					} else {
518
						$nextVersion = $version['version'] - $step;
519
						$prevTimestamp = $version['version'];
520
					}
521
					$newInterval = false; // version checked so we can move to the next one
522
				} else { // time to move on to the next interval
523
					$interval++;
524
					$step = Storage::$max_versions_per_interval[$interval]['step'];
525
					$nextVersion = $prevTimestamp - $step;
526 View Code Duplication
					if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
527
						$nextInterval = -1;
528
					} else {
529
						$nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
530
					}
531
					$newInterval = true; // we changed the interval -> check same version with new interval
532
				}
533
			}
534
		}
535
536
		return array($toDelete, $size);
537
538
	}
539
540
	/**
541
	 * Erase a file's versions which exceed the set quota
542
	 */
543
	private static function expire($filename, $versionsSize = null, $offset = 0) {
544
		$config = \OC::$server->getConfig();
545
		if($config->getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
546
			list($uid, $filename) = self::getUidAndFilename($filename);
547
			$versionsFileview = new \OC\Files\View('/'.$uid.'/files_versions');
548
549
			// get available disk space for user
550
			$softQuota = true;
551
			$quota = $config->getUserValue($uid, 'files', 'quota', null);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by self::getUidAndFilename($filename) on line 546 can also be of type false or null; however, OCP\IConfig::getUserValue() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
552 View Code Duplication
			if ( $quota === null || $quota === 'default') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
553
				$quota = $config->getAppValue('files', 'default_quota', null);
554
			}
555
			if ( $quota === null || $quota === 'none' ) {
556
				$quota = \OC\Files\Filesystem::free_space('/');
557
				$softQuota = false;
558
			} else {
559
				$quota = \OCP\Util::computerFileSize($quota);
560
			}
561
562
			// make sure that we have the current size of the version history
563
			if ( $versionsSize === null ) {
564
				$versionsSize = self::getVersionsSize($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by self::getUidAndFilename($filename) on line 546 can also be of type false or null; however, OCA\Files_Versions\Storage::getVersionsSize() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
565
			}
566
567
			// calculate available space for version history
568
			// subtract size of files and current versions size from quota
569
			if ($softQuota) {
570
				$files_view = new \OC\Files\View('/'.$uid.'/files');
571
				$rootInfo = $files_view->getFileInfo('/', false);
572
				$free = $quota-$rootInfo['size']; // remaining free space for user
573
				if ( $free > 0 ) {
574
					$availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - ($versionsSize + $offset); // how much space can be used for versions
575
				} else {
576
					$availableSpace = $free - $versionsSize - $offset;
577
				}
578
			} else {
579
				$availableSpace = $quota - $offset;
580
			}
581
582
			$allVersions = Storage::getVersions($uid, $filename);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by self::getUidAndFilename($filename) on line 546 can also be of type false or null; however, OCA\Files_Versions\Storage::getVersions() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
583
584
			$time = time();
585
			list($toDelete, $sizeOfDeletedVersions) = self::getExpireList($time, $allVersions);
586
587
			$availableSpace = $availableSpace + $sizeOfDeletedVersions;
588
			$versionsSize = $versionsSize - $sizeOfDeletedVersions;
589
590
			// if still not enough free space we rearrange the versions from all files
591
			if ($availableSpace <= 0) {
592
				$result = Storage::getAllVersions($uid);
0 ignored issues
show
Bug introduced by
It seems like $uid defined by self::getUidAndFilename($filename) on line 546 can also be of type false or null; however, OCA\Files_Versions\Storage::getAllVersions() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
593
				$allVersions = $result['all'];
594
595
				foreach ($result['by_file'] as $versions) {
596
					list($toDeleteNew, $size) = self::getExpireList($time, $versions);
597
					$toDelete = array_merge($toDelete, $toDeleteNew);
598
					$sizeOfDeletedVersions += $size;
599
				}
600
				$availableSpace = $availableSpace + $sizeOfDeletedVersions;
601
				$versionsSize = $versionsSize - $sizeOfDeletedVersions;
602
			}
603
604
			foreach($toDelete as $key => $path) {
605
				\OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $path));
606
				self::deleteVersion($versionsFileview, $path);
607
				\OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $path));
608
				unset($allVersions[$key]); // update array with the versions we keep
609
				\OCP\Util::writeLog('files_versions', "Expire: " . $path, \OCP\Util::DEBUG);
610
			}
611
612
			// Check if enough space is available after versions are rearranged.
613
			// If not we delete the oldest versions until we meet the size limit for versions,
614
			// but always keep the two latest versions
615
			$numOfVersions = count($allVersions) -2 ;
616
			$i = 0;
617
			// sort oldest first and make sure that we start at the first element
618
			ksort($allVersions);
619
			reset($allVersions);
620
			while ($availableSpace < 0 && $i < $numOfVersions) {
621
				$version = current($allVersions);
622
				\OC_Hook::emit('\OCP\Versions', 'preDelete', array('path' => $version['path'].'.v'.$version['version']));
623
				self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']);
624
				\OC_Hook::emit('\OCP\Versions', 'delete', array('path' => $version['path'].'.v'.$version['version']));
625
				\OCP\Util::writeLog('files_versions', 'running out of space! Delete oldest version: ' . $version['path'].'.v'.$version['version'] , \OCP\Util::DEBUG);
626
				$versionsSize -= $version['size'];
627
				$availableSpace += $version['size'];
628
				next($allVersions);
629
				$i++;
630
			}
631
632
			return $versionsSize; // finally return the new size of the version history
633
		}
634
635
		return false;
636
	}
637
638
	/**
639
	 * Create recursively missing directories inside of files_versions
640
	 * that match the given path to a file.
641
	 *
642
	 * @param string $filename $path to a file, relative to the user's
643
	 * "files" folder
644
	 * @param \OC\Files\View $view view on data/user/
645
	 */
646
	private static function createMissingDirectories($filename, $view) {
647
		$dirname = \OC\Files\Filesystem::normalizePath(dirname($filename));
648
		$dirParts = explode('/', $dirname);
649
		$dir = "/files_versions";
650 View Code Duplication
		foreach ($dirParts as $part) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
651
			$dir = $dir . '/' . $part;
652
			if (!$view->file_exists($dir)) {
653
				$view->mkdir($dir);
654
			}
655
		}
656
	}
657
658
}
659