diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index c6b5fe304ab95..b13006acbc1e6 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -32,6 +32,7 @@ OCA\Files\Command\Scan + OCA\Files\Command\ListFiles OCA\Files\Command\DeleteOrphanedFiles OCA\Files\Command\TransferOwnership OCA\Files\Command\ScanAppData diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index d5874228e4662..be2d8fbed41cf 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -38,7 +38,8 @@ 'OCA\\Files\\Command\\Object\\Put' => $baseDir . '/../lib/Command/Object/Put.php', 'OCA\\Files\\Command\\Put' => $baseDir . '/../lib/Command/Put.php', 'OCA\\Files\\Command\\RepairTree' => $baseDir . '/../lib/Command/RepairTree.php', - 'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', + 'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', + 'OCA\\Files\\Command\\ListFiles' => $baseDir . '/../lib/Command/ListFiles.php', 'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index d5b1947c1cc9e..9cb2f35980fa4 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -7,14 +7,14 @@ class ComposerStaticInitFiles { public static $prefixLengthsPsr4 = array ( - 'O' => + 'O' => array ( 'OCA\\Files\\' => 10, ), ); public static $prefixDirsPsr4 = array ( - 'OCA\\Files\\' => + 'OCA\\Files\\' => array ( 0 => __DIR__ . '/..' . '/../lib', ), @@ -53,7 +53,8 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Object\\Put' => __DIR__ . '/..' . '/../lib/Command/Object/Put.php', 'OCA\\Files\\Command\\Put' => __DIR__ . '/..' . '/../lib/Command/Put.php', 'OCA\\Files\\Command\\RepairTree' => __DIR__ . '/..' . '/../lib/Command/RepairTree.php', - 'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', + 'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', + 'OCA\\Files\\Command\\ListFiles' => __DIR__ . '/..' . '/../lib/Command/ListFiles.php', 'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php', diff --git a/apps/files/lib/Command/ListFiles.php b/apps/files/lib/Command/ListFiles.php new file mode 100644 index 0000000000000..8e0dbfde52e31 --- /dev/null +++ b/apps/files/lib/Command/ListFiles.php @@ -0,0 +1,312 @@ + + * @license AGPL-3.0-or-later + * + * 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 . + * + */ +namespace OCA\Files\Command; + +use OC\Core\Command\Base; +use OC\Core\Command\InterruptedException; +use OC\FilesMetadata\FilesMetadataManager; +use OC\ForbiddenException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\File; +use OCP\Files\FileInfo; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\NotFoundException; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ListFiles extends Base { + protected array $fileInfo = []; + protected array $dirInfo = []; + public function __construct( + private IUserManager $userManager, + private IRootFolder $rootFolder, + private FilesMetadataManager $filesMetadataManager, + private IEventDispatcher $eventDispatcher, + private LoggerInterface $logger + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + + $this->setName("files:list") + ->setDescription("List filesystem in the path mentioned in path argument") + ->addArgument( + "path", + InputArgument::REQUIRED, + 'List all the files and folder mentioned in this path, eg occ files:list path="/alice/files/Music", the path being a required argument to determine the user' + ) + ->addOption("type", "", InputArgument::OPTIONAL, "Filter by type like application, image, video etc") + ->addOption( + "minSize", + '0', + InputArgument::OPTIONAL, + "Filter by min size" + ) + ->addOption( + "maxSize", + '0', + InputArgument::OPTIONAL, + "Filter by max size" + ) + ->addOption( + "sort", + "name", + InputArgument::OPTIONAL, + "Sort by name, path, size, owner, type, perm, created-at" + ) + ->addOption("order", "ASC", InputArgument::OPTIONAL, "Order is either ASC or DESC"); + } + + private function getNodeInfo(File|Folder $node): array { + $nodeInfo = [ + "name" => $node->getName(), + "size" => $node->getSize() . " bytes", + "perm" => $node->getPermissions(), + "owner" => $node->getOwner()->getDisplayName(), + "created-at" => $node->getCreationTime(), + "type" => $node->getMimePart(), + ]; + if($node->getMimetype() == FileInfo::MIMETYPE_FOLDER) { + $nodeInfo['type'] = 'directory'; + } + + return $nodeInfo; + } + + protected function listFiles( + string $user, + string $path, + OutputInterface $output, + ?string $type = "", + ?int $minSize = 0, + ?int $maxSize = 0 + ): void { + try { + /** @var Folder $userFolder **/ + $userFolder = $this->rootFolder->get($path); + + $files = $userFolder->getDirectoryListing(); + foreach ($files as $file) { + $includeType = $includeMin = $includeMax = true; + $nodeInfo = $this->getNodeInfo($file); + if ($type != "" && $type != $nodeInfo['type']) { + $includeType = false; + } + if ($minSize > 0) { + $includeMin = $file->getSize() >= $minSize; + } + if ($maxSize > 0) { + $includeMax = $file->getSize() <= $maxSize; + } + if ($file instanceof File) { + if ($includeType && $includeMin && $includeMax) { + $this->fileInfo[] = $nodeInfo; + } + } elseif ($file instanceof Folder) { + if ($includeType && $includeMin && $includeMax) { + $this->dirInfo[] = $nodeInfo; + } + } + } + } catch (ForbiddenException $e) { + $output->writeln( + "Home storage for user $user not writable or 'files' subdirectory missing" + ); + $output->writeln(" " . $e->getMessage()); + $output->writeln( + 'Make sure you\'re running the list command only as the user the web server runs as' + ); + } catch (InterruptedException $e) { + # exit the function if ctrl-c has been pressed + $output->writeln("Interrupted by user"); + } catch (NotFoundException $e) { + $output->writeln( + "Path not found: " . $e->getMessage() . "" + ); + } catch (\Exception $e) { + $output->writeln( + "Exception during list: " . $e->getMessage() . "" + ); + $output->writeln("" . $e->getTraceAsString() . ""); + } + } + + protected function execute( + InputInterface $input, + OutputInterface $output + ): int { + $inputPath = $input->getArgument("path"); + if ($inputPath) { + $inputPath = ltrim($inputPath, "path="); + [, $user] = explode("/", rtrim($inputPath, "/").'/', 4); + } + + $this->initTools($output); + + if (is_object($user)) { + $user = $user->getUID(); + } + $path = $inputPath ?: "/" . $user; + + if ($this->userManager->userExists($user)) { + $output->writeln( + "Starting list for user ($user)" + ); + $this->listFiles( + $user, + $path, + $output, + $input->getOption("type"), + $input->getOption("minSize"), + $input->getOption("maxSize") + ); + } else { + $output->writeln( + "Unknown user $user" + ); + $output->writeln("", OutputInterface::VERBOSITY_VERBOSE); + return self::FAILURE; + } + + + $this->presentStats($input, $output); + return self::SUCCESS; + } + + /** + * Initialises some useful tools for the Command + */ + protected function initTools(OutputInterface $output): void { + // Convert PHP errors to exceptions + set_error_handler( + fn ( + int $severity, + string $message, + string $file, + int $line + ): bool => $this->exceptionErrorHandler( + $output, + $severity, + $message, + $file, + $line + ), + E_ALL + ); + } + + /** + * Processes PHP errors in order to be able to show them in the output + * + * @see https://www.php.net/manual/en/function.set-error-handler.php + * + * @param int $severity the level of the error raised + * @param string $message + * @param string $file the filename that the error was raised in + * @param int $line the line number the error was raised + */ + public function exceptionErrorHandler( + OutputInterface $output, + int $severity, + string $message, + string $file, + int $line + ): bool { + if ($severity === E_DEPRECATED || $severity === E_USER_DEPRECATED) { + // Do not show deprecation warnings + return false; + } + $e = new \ErrorException($message, 0, $severity, $file, $line); + $output->writeln( + "Error during list: " . $e->getMessage() . "" + ); + $output->writeln( + "" . $e->getTraceAsString() . "", + OutputInterface::VERBOSITY_VERY_VERBOSE + ); + return true; + } + + protected function presentStats( + InputInterface $input, + OutputInterface $output + ): void { + $headers = [ + "Permission", + "Size", + "Owner", + "Created at", + "Filename", + "Type", + ]; + $rows = []; + $fileInfo = $this->fileInfo[0] ?? []; + $sortKey = array_key_exists($input->getOption("sort"), $fileInfo) + ? $input->getOption("sort") + : "name"; + $order = $input->getOption("order") == "ASC" ? SORT_ASC : SORT_DESC; + $fileArr = array_column($this->fileInfo, $sortKey); + $dirArr = array_column($this->dirInfo, $sortKey); + array_multisort( + $fileArr, + $order, + $this->fileInfo + ); + array_multisort( + $dirArr, + $order, + $this->dirInfo + ); + + foreach ($this->fileInfo as $k => $item) { + $rows[$k] = [ + $item["perm"], + $item["size"], + $item["owner"], + $item["created-at"], + $item["name"], + $item["type"], + ]; + } + foreach ($this->dirInfo as $k => $item) { + $rows[] = [ + $item["perm"], + $item["size"], + $item["owner"], + $item["created-at"], + $item["name"], + $item["type"], + ]; + } + + $table = new Table($output); + $table->setHeaders($headers)->setRows($rows); + $table->render(); + } +}