From 923f2f6c75d3b7c52442ab62f7a24cd92b968044 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Jul 2026 23:05:00 +0200 Subject: [PATCH 1/5] feat: add occ command for creating folders Signed-off-by: Robin Appelman --- apps/files/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files/lib/Command/Mkdir.php | 54 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 apps/files/lib/Command/Mkdir.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index c710df2fb1723..48d122d9296e9 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -46,6 +46,7 @@ OCA\Files\Command\Delete OCA\Files\Command\Copy OCA\Files\Command\Move + OCA\Files\Command\Mkdir OCA\Files\Command\SanitizeFilenames OCA\Files\Command\Mount\Refresh OCA\Files\Command\Mount\ListMounts diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 6f4d17a78f13b..530d7a88055b8 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -32,6 +32,7 @@ 'OCA\\Files\\Command\\Delete' => $baseDir . '/../lib/Command/Delete.php', 'OCA\\Files\\Command\\DeleteOrphanedFiles' => $baseDir . '/../lib/Command/DeleteOrphanedFiles.php', 'OCA\\Files\\Command\\Get' => $baseDir . '/../lib/Command/Get.php', + 'OCA\\Files\\Command\\Mkdir' => $baseDir . '/../lib/Command/Mkdir.php', 'OCA\\Files\\Command\\Mount\\ListMounts' => $baseDir . '/../lib/Command/Mount/ListMounts.php', 'OCA\\Files\\Command\\Mount\\Refresh' => $baseDir . '/../lib/Command/Mount/Refresh.php', 'OCA\\Files\\Command\\Move' => $baseDir . '/../lib/Command/Move.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 9bcefe76c340f..2742c16126c75 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -47,6 +47,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Delete' => __DIR__ . '/..' . '/../lib/Command/Delete.php', 'OCA\\Files\\Command\\DeleteOrphanedFiles' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanedFiles.php', 'OCA\\Files\\Command\\Get' => __DIR__ . '/..' . '/../lib/Command/Get.php', + 'OCA\\Files\\Command\\Mkdir' => __DIR__ . '/..' . '/../lib/Command/Mkdir.php', 'OCA\\Files\\Command\\Mount\\ListMounts' => __DIR__ . '/..' . '/../lib/Command/Mount/ListMounts.php', 'OCA\\Files\\Command\\Mount\\Refresh' => __DIR__ . '/..' . '/../lib/Command/Mount/Refresh.php', 'OCA\\Files\\Command\\Move' => __DIR__ . '/..' . '/../lib/Command/Move.php', diff --git a/apps/files/lib/Command/Mkdir.php b/apps/files/lib/Command/Mkdir.php new file mode 100644 index 0000000000000..a7c47ccd362b7 --- /dev/null +++ b/apps/files/lib/Command/Mkdir.php @@ -0,0 +1,54 @@ +setName('files:mkdir') + ->setDescription('Create a new directory') + ->addArgument('path', InputArgument::REQUIRED, 'Target Nextcloud path for the new folder'); + } + + #[\Override] + public function execute(InputInterface $input, OutputInterface $output): int { + $path = $input->getArgument('path'); + $node = $this->fileUtils->getNode($path); + + if ($node instanceof Folder) { + $output->writeln("$path already exists"); + return self::SUCCESS; + } + if ($node instanceof File) { + $output->writeln("$path is a file"); + return self::FAILURE; + } + + $this->rootFolder->newFolder($path); + + return self::SUCCESS; + } +} From c7ed7fec016a079de6caff31a5b36c3a243a1fc8 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Jul 2026 23:19:42 +0200 Subject: [PATCH 2/5] feat: add occ files:touch command Signed-off-by: Robin Appelman --- apps/files/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/files/lib/Command/Touch.php | 94 +++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 apps/files/lib/Command/Touch.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index 48d122d9296e9..24ffe49a02f0b 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -47,6 +47,7 @@ OCA\Files\Command\Copy OCA\Files\Command\Move OCA\Files\Command\Mkdir + OCA\Files\Command\Touch OCA\Files\Command\SanitizeFilenames OCA\Files\Command\Mount\Refresh OCA\Files\Command\Mount\ListMounts diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 530d7a88055b8..2e2cb6da8a601 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -50,6 +50,7 @@ 'OCA\\Files\\Command\\SanitizeFilenames' => $baseDir . '/../lib/Command/SanitizeFilenames.php', 'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', + 'OCA\\Files\\Command\\Touch' => $baseDir . '/../lib/Command/Touch.php', 'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Command\\WindowsCompatibleFilenames' => $baseDir . '/../lib/Command/WindowsCompatibleFilenames.php', 'OCA\\Files\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 2742c16126c75..d7786af11072c 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -65,6 +65,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\SanitizeFilenames' => __DIR__ . '/..' . '/../lib/Command/SanitizeFilenames.php', 'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', + 'OCA\\Files\\Command\\Touch' => __DIR__ . '/..' . '/../lib/Command/Touch.php', 'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', 'OCA\\Files\\Command\\WindowsCompatibleFilenames' => __DIR__ . '/..' . '/../lib/Command/WindowsCompatibleFilenames.php', 'OCA\\Files\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php', diff --git a/apps/files/lib/Command/Touch.php b/apps/files/lib/Command/Touch.php new file mode 100644 index 0000000000000..bd0c328561f64 --- /dev/null +++ b/apps/files/lib/Command/Touch.php @@ -0,0 +1,94 @@ +setName('files:touch') + ->setDescription('Update the last modified date of a file or folder, or create an empty file') + ->addArgument('file', InputArgument::REQUIRED, 'Nextcloud path or fileid for the file or folder to change the modified date of') + ->addOption('date', 'd', InputOption::VALUE_REQUIRED, 'Time to use as modified date instead of the current time. Acceptable formats are: ISO8601, "YYYY-MM-DD" and Unix time in seconds.') + ->addOption('no-create', 'c', InputOption::VALUE_NONE, 'Don\'t create an empty file if the target path doesn\'t exist'); + } + + #[\Override] + public function execute(InputInterface $input, OutputInterface $output): int { + $fileInput = $input->getArgument('file'); + $node = $this->fileUtils->getNode($fileInput); + $date = $input->getOption('date'); + $noCreate = $input->getOption('no-create'); + + if (!$node) { + if ($noCreate || is_numeric($fileInput)) { + $output->writeln("$fileInput doesn't exist"); + return self::FAILURE; + } + $node = $this->rootFolder->newFile($fileInput); + } + + + if ($date) { + $mtime = $this->parseDateOption($date); + if (!$mtime) { + $output->writeln("Invalid date format '$date'. Acceptable formats are: ISO8601, \"YYYY-MM-DD\" and Unix time in seconds."); + } + } else { + $mtime = $this->clock->now(); + } + $node->touch($mtime->getTimestamp()); + + return self::SUCCESS; + } + + /** + * @return \DateTimeImmutable|false + */ + protected function parseDateOption(string $input) { + $date = false; + + // Handle Unix timestamp + if (filter_var($input, FILTER_VALIDATE_INT)) { + return new DateTimeImmutable('@' . $input); + } + + // ISO8601 + $date = DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, $input); + if ($date) { + return $date; + } + // With fractions + $date = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $input); + if ($date) { + return $date; + } + + // YYYY-MM-DD + return DateTimeImmutable::createFromFormat('!Y-m-d', $input); + } +} From 344776627b970ad10ec2362c58d6e3da2e37d424 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Jul 2026 23:22:24 +0200 Subject: [PATCH 3/5] feat: create missing parent folders in files:put Signed-off-by: Robin Appelman --- apps/files/lib/Command/Put.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/files/lib/Command/Put.php b/apps/files/lib/Command/Put.php index 6de11c7a881d8..7907a3c6d2ee0 100644 --- a/apps/files/lib/Command/Put.php +++ b/apps/files/lib/Command/Put.php @@ -62,6 +62,11 @@ public function execute(InputInterface $input, OutputInterface $output): int { } stream_copy_to_stream($source, $target); } else { + $parentPath = dirname($fileOutput); + if (!$this->rootFolder->nodeExists($parentPath)) { + $this->rootFolder->newFolder($parentPath); + } + $this->rootFolder->newFile($fileOutput, $source); } return self::SUCCESS; From a91d7cf0e5260e82f062327e230b18f45f891bdb Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Wed, 15 Jul 2026 23:26:20 +0200 Subject: [PATCH 4/5] feat: add option to skip trashbin in files:delete Signed-off-by: Robin Appelman --- apps/files/lib/Command/Delete.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/files/lib/Command/Delete.php b/apps/files/lib/Command/Delete.php index 0558a6778b795..65799a8f1d898 100644 --- a/apps/files/lib/Command/Delete.php +++ b/apps/files/lib/Command/Delete.php @@ -10,6 +10,7 @@ use OC\Core\Command\Info\FileUtils; use OCA\Files_Sharing\SharedStorage; +use OCA\Files_Trashbin\Trash\ITrashManager; use OCP\Files\Folder; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; @@ -21,7 +22,8 @@ class Delete extends Command { public function __construct( - private FileUtils $fileUtils, + private readonly FileUtils $fileUtils, + private readonly ITrashManager $trashManager, ) { parent::__construct(); } @@ -32,7 +34,8 @@ protected function configure(): void { ->setName('files:delete') ->setDescription('Delete a file or folder') ->addArgument('file', InputArgument::REQUIRED, 'File id or path') - ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings"); + ->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings") + ->addOption('skip-trash', null, InputOption::VALUE_NONE, 'Bypass the trashbin when deleting the file or folder'); } #[\Override] @@ -40,6 +43,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $fileInput = $input->getArgument('file'); $inputIsId = is_numeric($fileInput); $force = $input->getOption('force'); + $skipTrash = $input->getOption('skip-trash'); $node = $this->fileUtils->getNode($fileInput); if (!$node) { @@ -90,6 +94,10 @@ public function execute(InputInterface $input, OutputInterface $output): int { if ($deleteConfirmed) { if ($node->isDeletable()) { + if ($skipTrash && $this->trashManager) { + $this->trashManager->pauseTrash(); + } + $node->delete(); } else { $output->writeln('File cannot be deleted, insufficient permissions.'); From 9d03d0435c334bfd32e9452bae4c19f54d3d818d Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Thu, 16 Jul 2026 07:48:26 +0200 Subject: [PATCH 5/5] fix: Mark ITrashManager as optional This is not present when files_trashbin is disabled Signed-off-by: Carl Schwan --- apps/files/lib/Command/Delete.php | 2 +- apps/files/lib/Command/Touch.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/files/lib/Command/Delete.php b/apps/files/lib/Command/Delete.php index 65799a8f1d898..627b993bcce5d 100644 --- a/apps/files/lib/Command/Delete.php +++ b/apps/files/lib/Command/Delete.php @@ -23,7 +23,7 @@ class Delete extends Command { public function __construct( private readonly FileUtils $fileUtils, - private readonly ITrashManager $trashManager, + private readonly ?ITrashManager $trashManager = null, ) { parent::__construct(); } diff --git a/apps/files/lib/Command/Touch.php b/apps/files/lib/Command/Touch.php index bd0c328561f64..217ce721aa4b4 100644 --- a/apps/files/lib/Command/Touch.php +++ b/apps/files/lib/Command/Touch.php @@ -52,7 +52,6 @@ public function execute(InputInterface $input, OutputInterface $output): int { $node = $this->rootFolder->newFile($fileInput); } - if ($date) { $mtime = $this->parseDateOption($date); if (!$mtime) {