]> source.dussan.org Git - nextcloud-server.git/commitdiff
add command to summarize space usage
authorRobin Appelman <robin@icewind.nl>
Fri, 14 Apr 2023 17:16:29 +0000 (19:16 +0200)
committerRobin Appelman <robin@icewind.nl>
Thu, 4 May 2023 11:10:25 +0000 (13:10 +0200)
Signed-off-by: Robin Appelman <robin@icewind.nl>
core/Command/Info/File.php
core/Command/Info/FileUtils.php [new file with mode: 0644]
core/Command/Info/Space.php [new file with mode: 0644]
core/register_command.php

index bf7b9ae4e0a6d3d688bf048adefc0491f830cde6..8ca7a3d0264ee4059362c102eca6b605ae10ff6d 100644 (file)
@@ -5,13 +5,9 @@ declare(strict_types=1);
 namespace OC\Core\Command\Info;
 
 use OC\Files\ObjectStore\ObjectStoreStorage;
-use OCA\Circles\MountManager\CircleMount;
 use OCA\Files_External\Config\ExternalMountPoint;
-use OCA\Files_Sharing\SharedMount;
 use OCA\GroupFolders\Mount\GroupMountPoint;
-use OCP\Constants;
 use OCP\Files\Config\IUserMountCache;
-use OCP\Files\FileInfo;
 use OCP\Files\Folder;
 use OCP\Files\IHomeStorage;
 use OCP\Files\IRootFolder;
@@ -20,7 +16,6 @@ use OCP\Files\Node;
 use OCP\Files\NotFoundException;
 use OCP\IL10N;
 use OCP\L10N\IFactory;
-use OCP\Share\IShare;
 use OCP\Util;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputArgument;
@@ -29,14 +24,12 @@ use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 
 class File extends Command {
-       private IRootFolder $rootFolder;
-       private IUserMountCache $userMountCache;
        private IL10N $l10n;
+       private FileUtils $fileUtils;
 
-       public function __construct(IRootFolder $rootFolder, IUserMountCache $userMountCache, IFactory $l10nFactory) {
-               $this->rootFolder = $rootFolder;
-               $this->userMountCache = $userMountCache;
+       public function __construct(IFactory $l10nFactory, FileUtils $fileUtils) {
                $this->l10n = $l10nFactory->get("core");
+               $this->fileUtils = $fileUtils;
                parent::__construct();
        }
 
@@ -51,7 +44,7 @@ class File extends Command {
        public function execute(InputInterface $input, OutputInterface $output): int {
                $fileInput = $input->getArgument('file');
                $showChildren = $input->getOption('children');
-               $node = $this->getNode($fileInput);
+               $node = $this->fileUtils->getNode($fileInput);
                if (!$node) {
                        $output->writeln("<error>file $fileInput not found</error>");
                        return 1;
@@ -83,137 +76,22 @@ class File extends Command {
                }
                $this->outputStorageDetails($node->getMountPoint(), $node, $output);
 
-               $filesPerUser = $this->getFilesByUser($node);
+               $filesPerUser = $this->fileUtils->getFilesByUser($node);
                $output->writeln("");
                $output->writeln("The following users have access to the file");
                $output->writeln("");
                foreach ($filesPerUser as $user => $files) {
                        $output->writeln("$user:");
                        foreach ($files as $userFile) {
-                               $output->writeln("  " . $userFile->getPath() . ": " . $this->formatPermissions($userFile->getType(), $userFile->getPermissions()));
+                               $output->writeln("  " . $userFile->getPath() . ": " . $this->fileUtils->formatPermissions($userFile->getType(), $userFile->getPermissions()));
                                $mount = $userFile->getMountPoint();
-                               $output->writeln("    " . $this->formatMountType($mount));
+                               $output->writeln("    " . $this->fileUtils->formatMountType($mount));
                        }
                }
 
                return 0;
        }
 
-       private function getNode(string $fileInput): ?Node {
-               if (is_numeric($fileInput)) {
-                       $mounts = $this->userMountCache->getMountsForFileId((int)$fileInput);
-                       if (!$mounts) {
-                               return null;
-                       }
-                       $mount = $mounts[0];
-                       $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
-                       $nodes = $userFolder->getById((int)$fileInput);
-                       if (!$nodes) {
-                               return null;
-                       }
-                       return $nodes[0];
-               } else {
-                       try {
-                               return $this->rootFolder->get($fileInput);
-                       } catch (NotFoundException $e) {
-                               return null;
-                       }
-               }
-       }
-
-       /**
-        * @param FileInfo $file
-        * @return array<string, Node[]>
-        * @throws \OCP\Files\NotPermittedException
-        * @throws \OC\User\NoUserException
-        */
-       private function getFilesByUser(FileInfo $file): array {
-               $id = $file->getId();
-               if (!$id) {
-                       return [];
-               }
-
-               $mounts = $this->userMountCache->getMountsForFileId($id);
-               $result = [];
-               foreach ($mounts as $mount) {
-                       if (isset($result[$mount->getUser()->getUID()])) {
-                               continue;
-                       }
-
-                       $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
-                       $result[$mount->getUser()->getUID()] = $userFolder->getById($id);
-               }
-
-               return $result;
-       }
-
-       private function formatPermissions(string $type, int $permissions): string {
-               if ($permissions == Constants::PERMISSION_ALL || ($type === 'file' && $permissions == (Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE))) {
-                       return "full permissions";
-               }
-
-               $perms = [];
-               $allPerms = [Constants::PERMISSION_READ => "read", Constants::PERMISSION_UPDATE => "update", Constants::PERMISSION_CREATE => "create", Constants::PERMISSION_DELETE => "delete", Constants::PERMISSION_SHARE => "share"];
-               foreach ($allPerms as $perm => $name) {
-                       if (($permissions & $perm) === $perm) {
-                               $perms[] = $name;
-                       }
-               }
-
-               return implode(", ", $perms);
-       }
-
-       /**
-        * @psalm-suppress UndefinedClass
-        * @psalm-suppress UndefinedInterfaceMethod
-        */
-       private function formatMountType(IMountPoint $mountPoint): string {
-               $storage = $mountPoint->getStorage();
-               if ($storage && $storage->instanceOfStorage(IHomeStorage::class)) {
-                       return "home storage";
-               } elseif ($mountPoint instanceof SharedMount) {
-                       $share = $mountPoint->getShare();
-                       $shares = $mountPoint->getGroupedShares();
-                       $sharedBy = array_map(function (IShare $share) {
-                               $shareType = $this->formatShareType($share);
-                               if ($shareType) {
-                                       return $share->getSharedBy() . " (via " . $shareType . " " . $share->getSharedWith() . ")";
-                               } else {
-                                       return $share->getSharedBy();
-                               }
-                       }, $shares);
-                       $description = "shared by " . implode(', ', $sharedBy);
-                       if ($share->getSharedBy() !== $share->getShareOwner()) {
-                               $description .= " owned by " . $share->getShareOwner();
-                       }
-                       return $description;
-               } elseif ($mountPoint instanceof GroupMountPoint) {
-                       return "groupfolder " . $mountPoint->getFolderId();
-               } elseif ($mountPoint instanceof ExternalMountPoint) {
-                       return "external storage " . $mountPoint->getStorageConfig()->getId();
-               } elseif ($mountPoint instanceof CircleMount) {
-                       return "circle";
-               }
-               return get_class($mountPoint);
-       }
-
-       private function formatShareType(IShare $share): ?string {
-               switch ($share->getShareType()) {
-                       case IShare::TYPE_GROUP:
-                               return "group";
-                       case IShare::TYPE_CIRCLE:
-                               return "circle";
-                       case IShare::TYPE_DECK:
-                               return "deck";
-                       case IShare::TYPE_ROOM:
-                               return "room";
-                       case IShare::TYPE_USER:
-                               return null;
-                       default:
-                               return "Unknown (".$share->getShareType().")";
-               }
-       }
-
        /**
         * @psalm-suppress UndefinedClass
         * @psalm-suppress UndefinedInterfaceMethod
diff --git a/core/Command/Info/FileUtils.php b/core/Command/Info/FileUtils.php
new file mode 100644 (file)
index 0000000..e4793bd
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\Info;
+
+
+use OC\Files\SetupManager;
+use OCA\Circles\MountManager\CircleMount;
+use OCA\Files_External\Config\ExternalMountPoint;
+use OCA\Files_Sharing\SharedMount;
+use OCA\GroupFolders\Mount\GroupMountPoint;
+use OCP\Constants;
+use OCP\Files\Config\IUserMountCache;
+use OCP\Files\FileInfo;
+use OCP\Files\IHomeStorage;
+use OCP\Files\IRootFolder;
+use OCP\Files\Mount\IMountManager;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\Share\IShare;
+use OCP\Util;
+use Symfony\Component\Console\Output\OutputInterface;
+use OCP\Files\Folder;
+
+class FileUtils {
+       private IRootFolder $rootFolder;
+       private IUserMountCache $userMountCache;
+       private IMountManager $mountManager;
+       private SetupManager $setupManager;
+
+       public function __construct(
+               IRootFolder $rootFolder,
+               IUserMountCache $userMountCache,
+               IMountManager $mountManager,
+               SetupManager $setupManager
+       ) {
+               $this->rootFolder = $rootFolder;
+               $this->userMountCache = $userMountCache;
+               $this->mountManager = $mountManager;
+               $this->setupManager = $setupManager;
+       }
+
+       /**
+        * @param FileInfo $file
+        * @return array<string, Node[]>
+        * @throws \OCP\Files\NotPermittedException
+        * @throws \OC\User\NoUserException
+        */
+       public function getFilesByUser(FileInfo $file): array {
+               $id = $file->getId();
+               if (!$id) {
+                       return [];
+               }
+
+               $mounts = $this->userMountCache->getMountsForFileId($id);
+               $result = [];
+               foreach ($mounts as $mount) {
+                       if (isset($result[$mount->getUser()->getUID()])) {
+                               continue;
+                       }
+
+                       $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
+                       $result[$mount->getUser()->getUID()] = $userFolder->getById($id);
+               }
+
+               return $result;
+       }
+
+       /**
+        * Get file by either id of path
+        *
+        * @param string $fileInput
+        * @return Node|null
+        */
+       public function getNode(string $fileInput): ?Node {
+               if (is_numeric($fileInput)) {
+                       $mounts = $this->userMountCache->getMountsForFileId((int)$fileInput);
+                       if (!$mounts) {
+                               return null;
+                       }
+                       $mount = $mounts[0];
+                       $userFolder = $this->rootFolder->getUserFolder($mount->getUser()->getUID());
+                       $nodes = $userFolder->getById((int)$fileInput);
+                       if (!$nodes) {
+                               return null;
+                       }
+                       return $nodes[0];
+               } else {
+                       try {
+                               return $this->rootFolder->get($fileInput);
+                       } catch (NotFoundException $e) {
+                               return null;
+                       }
+               }
+       }
+
+       public function formatPermissions(string $type, int $permissions): string {
+               if ($permissions == Constants::PERMISSION_ALL || ($type === 'file' && $permissions == (Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE))) {
+                       return "full permissions";
+               }
+
+               $perms = [];
+               $allPerms = [Constants::PERMISSION_READ => "read", Constants::PERMISSION_UPDATE => "update", Constants::PERMISSION_CREATE => "create", Constants::PERMISSION_DELETE => "delete", Constants::PERMISSION_SHARE => "share"];
+               foreach ($allPerms as $perm => $name) {
+                       if (($permissions & $perm) === $perm) {
+                               $perms[] = $name;
+                       }
+               }
+
+               return implode(", ", $perms);
+       }
+
+       /**
+        * @psalm-suppress UndefinedClass
+        * @psalm-suppress UndefinedInterfaceMethod
+        */
+       public function formatMountType(IMountPoint $mountPoint): string {
+               $storage = $mountPoint->getStorage();
+               if ($storage && $storage->instanceOfStorage(IHomeStorage::class)) {
+                       return "home storage";
+               } elseif ($mountPoint instanceof SharedMount) {
+                       $share = $mountPoint->getShare();
+                       $shares = $mountPoint->getGroupedShares();
+                       $sharedBy = array_map(function (IShare $share) {
+                               $shareType = $this->formatShareType($share);
+                               if ($shareType) {
+                                       return $share->getSharedBy() . " (via " . $shareType . " " . $share->getSharedWith() . ")";
+                               } else {
+                                       return $share->getSharedBy();
+                               }
+                       }, $shares);
+                       $description = "shared by " . implode(', ', $sharedBy);
+                       if ($share->getSharedBy() !== $share->getShareOwner()) {
+                               $description .= " owned by " . $share->getShareOwner();
+                       }
+                       return $description;
+               } elseif ($mountPoint instanceof GroupMountPoint) {
+                       return "groupfolder " . $mountPoint->getFolderId();
+               } elseif ($mountPoint instanceof ExternalMountPoint) {
+                       return "external storage " . $mountPoint->getStorageConfig()->getId();
+               } elseif ($mountPoint instanceof CircleMount) {
+                       return "circle";
+               }
+               return get_class($mountPoint);
+       }
+
+       public function formatShareType(IShare $share): ?string {
+               switch ($share->getShareType()) {
+                       case IShare::TYPE_GROUP:
+                               return "group";
+                       case IShare::TYPE_CIRCLE:
+                               return "circle";
+                       case IShare::TYPE_DECK:
+                               return "deck";
+                       case IShare::TYPE_ROOM:
+                               return "room";
+                       case IShare::TYPE_USER:
+                               return null;
+                       default:
+                               return "Unknown (" . $share->getShareType() . ")";
+               }
+       }
+
+       /**
+        * Print out the largest count($sizeLimits) files in the directory tree
+        *
+        * @param OutputInterface $output
+        * @param Folder $node
+        * @param string $prefix
+        * @param array $sizeLimits largest items that are still in the queue to be printed, ordered ascending
+        * @return int how many items we've printed
+        */
+       public function outputLargeFilesTree(
+               OutputInterface $output,
+               Folder $node,
+               string $prefix,
+               array &$sizeLimits
+       ): int {
+               /**
+                * Algorithm to print the N largest items in a folder without requiring to query or sort the entire three
+                *
+                * This is done by keeping a list ($sizeLimits) of size N that contain the largest items outside of this
+                * folders that are could be printed if there aren't enough items in this folder that are larger.
+                *
+                * We loop over the items in this folder by size descending until the size of the item falls before the smallest
+                * size in $sizeLimits (at that point there are enough items outside this folder to complete the N items).
+                *
+                * When encountering a folder, we create an updated $sizeLimits with the largest items in the current folder still
+                * remaining which we pass into the recursion. (We don't update the current $sizeLimits because that should only
+                * hold items *outside* of the current folder.)
+                *
+                * For every item printed we remove the first item of $sizeLimits are there is no longer room in the output to print
+                * items that small.
+                */
+
+               $count = 0;
+               $children = $node->getDirectoryListing();
+               usort($children, function (Node $a, Node $b) {
+                       return $b->getSize() <=> $a->getSize();
+               });
+               foreach ($children as $i => $child) {
+                       if (count($sizeLimits) === 0 || $child->getSize() < $sizeLimits[0]) {
+                               return $count;
+                       }
+                       array_shift($sizeLimits);
+                       $count += 1;
+
+                       /** @var Node $child */
+                       $output->writeln("$prefix- " . $child->getName() . ": <info>" . Util::humanFileSize($child->getSize()) . "</info>");
+                       if ($child instanceof Folder) {
+                               $recurseSizeLimits = $sizeLimits;
+                               for ($j = 0; $j < count($recurseSizeLimits); $j++) {
+                                       $nextChildSize = (int)$children[$i + $j + 1]?->getSize();
+                                       if ($nextChildSize > $recurseSizeLimits[0]) {
+                                               array_shift($recurseSizeLimits);
+                                               $recurseSizeLimits[] = $nextChildSize;
+                                       }
+                               }
+                               sort($recurseSizeLimits);
+                               $recurseCount = $this->outputLargeFilesTree($output, $child, $prefix . "  ", $recurseSizeLimits);
+                               $sizeLimits = array_slice($sizeLimits, $recurseCount);
+                               $count += $recurseCount;
+                       }
+               }
+               return $count;
+       }
+}
diff --git a/core/Command/Info/Space.php b/core/Command/Info/Space.php
new file mode 100644 (file)
index 0000000..36821ac
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Core\Command\Info;
+
+
+use OCP\Files\Folder;
+use OCP\Util;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Space extends Command {
+       private FileUtils $fileUtils;
+
+       public function __construct(FileUtils $fileUtils) {
+               $this->fileUtils = $fileUtils;
+               parent::__construct();
+       }
+
+       protected function configure(): void {
+               $this
+                       ->setName('info:file:space')
+                       ->setDescription('Summarize space usage of specified folder')
+                       ->addArgument('file', InputArgument::REQUIRED, "File id or path")
+                       ->addOption('count', 'c', InputOption::VALUE_REQUIRED, "Number of items to display", 25);
+       }
+
+       public function execute(InputInterface $input, OutputInterface $output): int {
+               $fileInput = $input->getArgument('file');
+               $count = (int)$input->getOption('count');
+               $node = $this->fileUtils->getNode($fileInput);
+               if (!$node) {
+                       $output->writeln("<error>file $fileInput not found</error>");
+                       return 1;
+               }
+               $output->writeln($node->getName() . ": <info>" . Util::humanFileSize($node->getSize()) . "</info>");
+               if ($node instanceof Folder) {
+                       $limits = array_fill(0, $count - 1, 0);
+                       $this->fileUtils->outputLargeFilesTree($output, $node, '', $limits);
+               }
+               return 0;
+       }
+
+}
index 4aac7fbf8ceb7bea2ac3266b11257cc664deb71e..8f600d7b8948865e242e89a8d1c004011f756796 100644 (file)
@@ -104,6 +104,7 @@ if (\OC::$server->getConfig()->getSystemValue('installed', false)) {
        $application->add(new OC\Core\Command\Config\System\SetConfig(\OC::$server->getSystemConfig()));
 
        $application->add(\OC::$server->get(OC\Core\Command\Info\File::class));
+       $application->add(\OC::$server->get(OC\Core\Command\Info\Space::class));
 
        $application->add(new OC\Core\Command\Db\ConvertType(\OC::$server->getConfig(), new \OC\DB\ConnectionFactory(\OC::$server->getSystemConfig())));
        $application->add(new OC\Core\Command\Db\ConvertMysqlToMB4(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection(), \OC::$server->getURLGenerator(), \OC::$server->get(LoggerInterface::class)));