Total Complexity | 113 |
Total Lines | 905 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Storage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Storage, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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) { |
||
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) { |
||
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 { |
||
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) { |
||
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) { |
||
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 | try { |
||
447 | // TODO add a proper way of overwriting a file while maintaining file ids |
||
448 | if ($storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage') || $storage2->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')) { |
||
449 | $source = $storage1->fopen($internalPath1, 'r'); |
||
450 | $target = $storage2->fopen($internalPath2, 'w'); |
||
451 | [, $result] = \OC_Helper::streamCopy($source, $target); |
||
452 | fclose($source); |
||
453 | fclose($target); |
||
454 | |||
455 | if ($result !== false) { |
||
456 | $storage1->unlink($internalPath1); |
||
457 | } |
||
458 | } else { |
||
459 | $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); |
||
460 | } |
||
461 | } finally { |
||
462 | $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); |
||
463 | $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); |
||
464 | } |
||
465 | |||
466 | return ($result !== false); |
||
467 | } |
||
468 | |||
469 | /** |
||
470 | * get a list of all available versions of a file in descending chronological order |
||
471 | * @param string $uid user id from the owner of the file |
||
472 | * @param string $filename file to find versions of, relative to the user files dir |
||
473 | * @param string $userFullPath |
||
474 | * @return array versions newest version first |
||
475 | */ |
||
476 | public static function getVersions($uid, $filename, $userFullPath = '') { |
||
542 | } |
||
543 | |||
544 | /** |
||
545 | * Expire versions that older than max version retention time |
||
546 | * |
||
547 | * @param string $uid |
||
548 | */ |
||
549 | public static function expireOlderThanMaxForUser($uid) { |
||
550 | /** @var IRootFolder $root */ |
||
551 | $root = \OC::$server->get(IRootFolder::class); |
||
552 | try { |
||
553 | /** @var Folder $versionsRoot */ |
||
554 | $versionsRoot = $root->get('/' . $uid . '/files_versions'); |
||
555 | } catch (NotFoundException $e) { |
||
556 | return; |
||
557 | } |
||
558 | |||
559 | $expiration = self::getExpiration(); |
||
560 | $threshold = $expiration->getMaxAgeAsTimestamp(); |
||
561 | if (!$threshold) { |
||
562 | return; |
||
563 | } |
||
564 | |||
565 | $allVersions = $versionsRoot->search(new SearchQuery( |
||
566 | new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_NOT, [ |
||
567 | new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', FileInfo::MIMETYPE_FOLDER), |
||
568 | ]), |
||
569 | 0, |
||
570 | 0, |
||
571 | [] |
||
572 | )); |
||
573 | |||
574 | /** @var VersionsMapper $versionsMapper */ |
||
575 | $versionsMapper = \OC::$server->get(VersionsMapper::class); |
||
576 | $userFolder = $root->getUserFolder($uid); |
||
577 | $versionEntities = []; |
||
578 | |||
579 | /** @var Node[] $versions */ |
||
580 | $versions = array_filter($allVersions, function (Node $info) use ($threshold, $userFolder, $versionsMapper, $versionsRoot, &$versionEntities) { |
||
581 | // Check that the file match '*.v*' |
||
582 | $versionsBegin = strrpos($info->getName(), '.v'); |
||
583 | if ($versionsBegin === false) { |
||
584 | return false; |
||
585 | } |
||
586 | |||
587 | $version = (int)substr($info->getName(), $versionsBegin + 2); |
||
588 | |||
589 | // Check that the version does not have a label. |
||
590 | $path = $versionsRoot->getRelativePath($info->getPath()); |
||
591 | if ($path === null) { |
||
592 | throw new DoesNotExistException('Could not find relative path of (' . $info->getPath() . ')'); |
||
593 | } |
||
594 | |||
595 | $node = $userFolder->get(substr($path, 0, -strlen('.v'.$version))); |
||
596 | try { |
||
597 | $versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version); |
||
598 | $versionEntities[$info->getId()] = $versionEntity; |
||
599 | |||
600 | if ($versionEntity->getLabel() !== '') { |
||
601 | return false; |
||
602 | } |
||
603 | } catch (DoesNotExistException $ex) { |
||
604 | // Version on FS can have no equivalent in the DB if they were created before the version naming feature. |
||
605 | // So we ignore DoesNotExistException. |
||
606 | } |
||
607 | |||
608 | // Check that the version's timestamp is lower than $threshold |
||
609 | return $version < $threshold; |
||
610 | }); |
||
611 | |||
612 | foreach ($versions as $version) { |
||
613 | $internalPath = $version->getInternalPath(); |
||
614 | \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]); |
||
615 | |||
616 | $versionEntity = isset($versionEntities[$version->getId()]) ? $versionEntities[$version->getId()] : null; |
||
617 | if (!is_null($versionEntity)) { |
||
618 | $versionsMapper->delete($versionEntity); |
||
619 | } |
||
620 | |||
621 | $version->delete(); |
||
622 | \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]); |
||
623 | } |
||
624 | } |
||
625 | |||
626 | /** |
||
627 | * translate a timestamp into a string like "5 days ago" |
||
628 | * |
||
629 | * @param int $timestamp |
||
630 | * @return string for example "5 days ago" |
||
631 | */ |
||
632 | private static function getHumanReadableTimestamp(int $timestamp): string { |
||
633 | $diff = time() - $timestamp; |
||
634 | |||
635 | if ($diff < 60) { // first minute |
||
636 | return $diff . " seconds ago"; |
||
637 | } elseif ($diff < 3600) { //first hour |
||
638 | return round($diff / 60) . " minutes ago"; |
||
639 | } elseif ($diff < 86400) { // first day |
||
640 | return round($diff / 3600) . " hours ago"; |
||
641 | } elseif ($diff < 604800) { //first week |
||
642 | return round($diff / 86400) . " days ago"; |
||
643 | } elseif ($diff < 2419200) { //first month |
||
644 | return round($diff / 604800) . " weeks ago"; |
||
645 | } elseif ($diff < 29030400) { // first year |
||
646 | return round($diff / 2419200) . " months ago"; |
||
647 | } else { |
||
648 | return round($diff / 29030400) . " years ago"; |
||
649 | } |
||
650 | } |
||
651 | |||
652 | /** |
||
653 | * returns all stored file versions from a given user |
||
654 | * @param string $uid id of the user |
||
655 | * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename |
||
656 | */ |
||
657 | private static function getAllVersions($uid) { |
||
658 | $view = new View('/' . $uid . '/'); |
||
659 | $dirs = [self::VERSIONS_ROOT]; |
||
660 | $versions = []; |
||
661 | |||
662 | while (!empty($dirs)) { |
||
663 | $dir = array_pop($dirs); |
||
664 | $files = $view->getDirectoryContent($dir); |
||
665 | |||
666 | foreach ($files as $file) { |
||
667 | $fileData = $file->getData(); |
||
668 | $filePath = $dir . '/' . $fileData['name']; |
||
669 | if ($file['type'] === 'dir') { |
||
670 | $dirs[] = $filePath; |
||
671 | } else { |
||
672 | $versionsBegin = strrpos($filePath, '.v'); |
||
673 | $relPathStart = strlen(self::VERSIONS_ROOT); |
||
674 | $version = substr($filePath, $versionsBegin + 2); |
||
675 | $relpath = substr($filePath, $relPathStart, $versionsBegin - $relPathStart); |
||
676 | $key = $version . '#' . $relpath; |
||
677 | $versions[$key] = ['path' => $relpath, 'timestamp' => $version]; |
||
678 | } |
||
679 | } |
||
680 | } |
||
681 | |||
682 | // newest version first |
||
683 | krsort($versions); |
||
684 | |||
685 | $result = [ |
||
686 | 'all' => [], |
||
687 | 'by_file' => [], |
||
688 | ]; |
||
689 | |||
690 | foreach ($versions as $key => $value) { |
||
691 | $size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']); |
||
692 | $filename = $value['path']; |
||
693 | |||
694 | $result['all'][$key]['version'] = $value['timestamp']; |
||
695 | $result['all'][$key]['path'] = $filename; |
||
696 | $result['all'][$key]['size'] = $size; |
||
697 | |||
698 | $result['by_file'][$filename][$key]['version'] = $value['timestamp']; |
||
699 | $result['by_file'][$filename][$key]['path'] = $filename; |
||
700 | $result['by_file'][$filename][$key]['size'] = $size; |
||
701 | } |
||
702 | |||
703 | return $result; |
||
704 | } |
||
705 | |||
706 | /** |
||
707 | * get list of files we want to expire |
||
708 | * @param array $versions list of versions |
||
709 | * @param integer $time |
||
710 | * @param bool $quotaExceeded is versions storage limit reached |
||
711 | * @return array containing the list of to deleted versions and the size of them |
||
712 | */ |
||
713 | protected static function getExpireList($time, $versions, $quotaExceeded = false) { |
||
714 | $expiration = self::getExpiration(); |
||
715 | |||
716 | if ($expiration->shouldAutoExpire()) { |
||
717 | [$toDelete, $size] = self::getAutoExpireList($time, $versions); |
||
718 | } else { |
||
719 | $size = 0; |
||
720 | $toDelete = []; // versions we want to delete |
||
721 | } |
||
722 | |||
723 | foreach ($versions as $key => $version) { |
||
724 | if (!is_numeric($version['version'])) { |
||
725 | \OC::$server->get(LoggerInterface::class)->error( |
||
726 | 'Found a non-numeric timestamp version: '. json_encode($version), |
||
727 | ['app' => 'files_versions']); |
||
728 | continue; |
||
729 | } |
||
730 | if ($expiration->isExpired((int)($version['version']), $quotaExceeded) && !isset($toDelete[$key])) { |
||
731 | $size += $version['size']; |
||
732 | $toDelete[$key] = $version['path'] . '.v' . $version['version']; |
||
733 | } |
||
734 | } |
||
735 | |||
736 | return [$toDelete, $size]; |
||
737 | } |
||
738 | |||
739 | /** |
||
740 | * get list of files we want to expire |
||
741 | * @param array $versions list of versions |
||
742 | * @param integer $time |
||
743 | * @return array containing the list of to deleted versions and the size of them |
||
744 | */ |
||
745 | protected static function getAutoExpireList($time, $versions) { |
||
797 | } |
||
798 | |||
799 | /** |
||
800 | * Schedule versions expiration for the given file |
||
801 | * |
||
802 | * @param string $uid owner of the file |
||
803 | * @param string $fileName file/folder for which to schedule expiration |
||
804 | */ |
||
805 | public static function scheduleExpire($uid, $fileName) { |
||
806 | // let the admin disable auto expire |
||
807 | $expiration = self::getExpiration(); |
||
808 | if ($expiration->isEnabled()) { |
||
809 | $command = new Expire($uid, $fileName); |
||
810 | /** @var IBus $bus */ |
||
811 | $bus = \OC::$server->get(IBus::class); |
||
812 | $bus->push($command); |
||
813 | } |
||
814 | } |
||
815 | |||
816 | /** |
||
817 | * Expire versions which exceed the quota. |
||
818 | * |
||
819 | * This will setup the filesystem for the given user but will not |
||
820 | * tear it down afterwards. |
||
821 | * |
||
822 | * @param string $filename path to file to expire |
||
823 | * @param string $uid user for which to expire the version |
||
824 | * @return bool|int|null |
||
825 | */ |
||
826 | public static function expire($filename, $uid) { |
||
947 | } |
||
948 | |||
949 | /** |
||
950 | * Create recursively missing directories inside of files_versions |
||
951 | * that match the given path to a file. |
||
952 | * |
||
953 | * @param string $filename $path to a file, relative to the user's |
||
954 | * "files" folder |
||
955 | * @param View $view view on data/user/ |
||
956 | */ |
||
957 | public static function createMissingDirectories($filename, $view) { |
||
965 | } |
||
966 | } |
||
967 | } |
||
968 | |||
969 | /** |
||
970 | * Static workaround |
||
971 | * @return Expiration |
||
972 | */ |
||
973 | protected static function getExpiration() { |
||
978 | } |
||
979 | } |
||
980 |