-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Feature/files list occ command #43342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
027d692
b9caee2
f90b106
1503053
2b915f3
7e27fbe
d82b3b3
a035d41
ac4efa8
92e5516
d59769e
1764794
5638ef2
79b5410
4058838
90f5d26
351d671
0cfb4dd
fc20909
14078fc
9b129a6
57752e6
e8573aa
c5ded86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,304 @@ | ||||||
| <?php | ||||||
| /** | ||||||
| * @copyright Copyright (c) 2024, Nextcloud GmbH | ||||||
| * | ||||||
| * @author Kareem <[email protected]> | ||||||
| * @license AGPL-3.0 | ||||||
| * | ||||||
| * This code is free software: you can redistribute it and/or modify | ||||||
| * it under the terms of the GNU Affero General Public License, version 3, | ||||||
| * as published by the Free Software Foundation. | ||||||
| * | ||||||
| * 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, version 3, | ||||||
| * along with this program. If not, see <http://www.gnu.org/licenses/> | ||||||
| * | ||||||
| */ | ||||||
| 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\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") | ||||||
| ->addArgument( | ||||||
| "path", | ||||||
| InputArgument::REQUIRED, | ||||||
| 'Limit list to this path, eg. path="/alice/files/Music", the user_id is determined by the path parameter' | ||||||
|
||||||
| ) | ||||||
| ->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 { | ||||||
| return [ | ||||||
| "name" => $node->getName(), | ||||||
| "size" => $node->getSize() . " bytes", | ||||||
| "perm" => $node->getPermissions(), | ||||||
| "owner" => $node->getOwner()->getDisplayName(), | ||||||
Check noticeCode scanning / Psalm PossiblyNullReference
Cannot call method getDisplayName on possibly null value
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| "created-at" => $node->getCreationTime(), | ||||||
| "type" => $node->getMimePart(), | ||||||
yemkareems marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| ]; | ||||||
| } | ||||||
|
|
||||||
| protected function listFiles( | ||||||
| string $user, | ||||||
| string $path, | ||||||
| OutputInterface $output, | ||||||
| ?string $type = "", | ||||||
| ?int $minSize = 0, | ||||||
| ?int $maxSize = 0 | ||||||
| ): void { | ||||||
| try { | ||||||
| /** @ var OC\Files\Node\Folder $userFolder **/ | ||||||
artonge marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| $userFolder = $this->rootFolder->get($path); | ||||||
|
||||||
|
|
||||||
| $files = $userFolder->getDirectoryListing(); | ||||||
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||||||
| foreach ($files as $file) { | ||||||
| $includeType = $includeMin = $includeMax = true; | ||||||
| if ($type != "" && $type != $file->getMimePart()) { | ||||||
| $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[] = $this->getNodeInfo($file); | ||||||
|
||||||
| } | ||||||
| } elseif ($file instanceof Folder) { | ||||||
| if ($includeType && $includeMin && $includeMax) { | ||||||
| $this->dirInfo[] = $this->getNodeInfo($file); | ||||||
|
||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } catch (ForbiddenException $e) { | ||||||
| $output->writeln( | ||||||
| "<error>Home storage for user $user not writable or 'files' subdirectory missing</error>" | ||||||
| ); | ||||||
| $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( | ||||||
| "<error>Path not found: " . $e->getMessage() . "</error>" | ||||||
| ); | ||||||
| } catch (\Exception $e) { | ||||||
| $output->writeln( | ||||||
| "<error>Exception during list: " . $e->getMessage() . "</error>" | ||||||
| ); | ||||||
| $output->writeln("<error>" . $e->getTraceAsString() . "</error>"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| 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)) { | ||||||
Check noticeCode scanning / Psalm PossiblyUndefinedVariable
Possibly undefined variable $user, first seen on line 158
|
||||||
| $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( | ||||||
| "<error>Unknown user $user</error>" | ||||||
| ); | ||||||
| $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>Error during list: " . $e->getMessage() . "</error>" | ||||||
| ); | ||||||
| $output->writeln( | ||||||
| "<error>" . $e->getTraceAsString() . "</error>", | ||||||
| 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(); | ||||||
| } | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.