Skip to content

Commit 4822f10

Browse files
authored
Merge pull request #37971 from nextcloud/info-file-get
Add commands for basic file actions
2 parents 9e53934 + ddc53a9 commit 4822f10

6 files changed

Lines changed: 300 additions & 0 deletions

File tree

apps/files/appinfo/info.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
<command>OCA\Files\Command\TransferOwnership</command>
3636
<command>OCA\Files\Command\ScanAppData</command>
3737
<command>OCA\Files\Command\RepairTree</command>
38+
<command>OCA\Files\Command\Get</command>
39+
<command>OCA\Files\Command\Put</command>
40+
<command>OCA\Files\Command\Delete</command>
3841
</commands>
3942

4043
<activity>

apps/files/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@
2727
'OCA\\Files\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
2828
'OCA\\Files\\Collaboration\\Resources\\Listener' => $baseDir . '/../lib/Collaboration/Resources/Listener.php',
2929
'OCA\\Files\\Collaboration\\Resources\\ResourceProvider' => $baseDir . '/../lib/Collaboration/Resources/ResourceProvider.php',
30+
'OCA\\Files\\Command\\Delete' => $baseDir . '/../lib/Command/Delete.php',
3031
'OCA\\Files\\Command\\DeleteOrphanedFiles' => $baseDir . '/../lib/Command/DeleteOrphanedFiles.php',
32+
'OCA\\Files\\Command\\Get' => $baseDir . '/../lib/Command/Get.php',
33+
'OCA\\Files\\Command\\Put' => $baseDir . '/../lib/Command/Put.php',
3134
'OCA\\Files\\Command\\RepairTree' => $baseDir . '/../lib/Command/RepairTree.php',
3235
'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php',
3336
'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ class ComposerStaticInitFiles
4242
'OCA\\Files\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
4343
'OCA\\Files\\Collaboration\\Resources\\Listener' => __DIR__ . '/..' . '/../lib/Collaboration/Resources/Listener.php',
4444
'OCA\\Files\\Collaboration\\Resources\\ResourceProvider' => __DIR__ . '/..' . '/../lib/Collaboration/Resources/ResourceProvider.php',
45+
'OCA\\Files\\Command\\Delete' => __DIR__ . '/..' . '/../lib/Command/Delete.php',
4546
'OCA\\Files\\Command\\DeleteOrphanedFiles' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanedFiles.php',
47+
'OCA\\Files\\Command\\Get' => __DIR__ . '/..' . '/../lib/Command/Get.php',
48+
'OCA\\Files\\Command\\Put' => __DIR__ . '/..' . '/../lib/Command/Put.php',
4649
'OCA\\Files\\Command\\RepairTree' => __DIR__ . '/..' . '/../lib/Command/RepairTree.php',
4750
'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php',
4851
'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php',

apps/files/lib/Command/Delete.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
namespace OCA\Files\Command;
25+
26+
use OC\Core\Command\Info\FileUtils;
27+
use OCA\Files_Sharing\SharedStorage;
28+
use OCP\Files\Folder;
29+
use Symfony\Component\Console\Command\Command;
30+
use Symfony\Component\Console\Helper\QuestionHelper;
31+
use Symfony\Component\Console\Input\InputArgument;
32+
use Symfony\Component\Console\Input\InputInterface;
33+
use Symfony\Component\Console\Input\InputOption;
34+
use Symfony\Component\Console\Output\OutputInterface;
35+
use Symfony\Component\Console\Question\ConfirmationQuestion;
36+
37+
class Delete extends Command {
38+
private FileUtils $fileUtils;
39+
40+
public function __construct(FileUtils $fileUtils) {
41+
$this->fileUtils = $fileUtils;
42+
parent::__construct();
43+
}
44+
45+
protected function configure(): void {
46+
$this
47+
->setName('files:delete')
48+
->setDescription('Delete a file or folder')
49+
->addArgument('file', InputArgument::REQUIRED, "File id or path")
50+
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings");
51+
}
52+
53+
public function execute(InputInterface $input, OutputInterface $output): int {
54+
$fileInput = $input->getArgument('file');
55+
$inputIsId = is_numeric($fileInput);
56+
$force = $input->getOption('force');
57+
$node = $this->fileUtils->getNode($fileInput);
58+
59+
if (!$node) {
60+
$output->writeln("<error>file $fileInput not found</error>");
61+
return 1;
62+
}
63+
64+
$deleteConfirmed = $force;
65+
if (!$deleteConfirmed) {
66+
/** @var QuestionHelper $helper */
67+
$helper = $this->getHelper('question');
68+
$storage = $node->getStorage();
69+
if (!$inputIsId && $storage->instanceOfStorage(SharedStorage::class) && $node->getInternalPath() === '') {
70+
/** @var SharedStorage $storage */
71+
[,$user] = explode('/', $fileInput, 3);
72+
$question = new ConfirmationQuestion("<info>$fileInput</info> in a shared file, do you want to unshare the file from <info>$user</info> instead of deleting the source file? [Y/n] ", true);
73+
if ($helper->ask($input, $output, $question)) {
74+
$storage->unshareStorage();
75+
return 0;
76+
} else {
77+
$node = $storage->getShare()->getNode();
78+
$output->writeln("");
79+
}
80+
}
81+
82+
$filesByUsers = $this->fileUtils->getFilesByUser($node);
83+
if (count($filesByUsers) > 1) {
84+
$output->writeln("Warning: the provided file is accessible by more than one user");
85+
$output->writeln(" all of the following users will lose access to the file when deleted:");
86+
$output->writeln("");
87+
foreach ($filesByUsers as $user => $filesByUser) {
88+
$output->writeln($user . ":");
89+
foreach($filesByUser as $file) {
90+
$output->writeln(" - " . $file->getPath());
91+
}
92+
}
93+
$output->writeln("");
94+
}
95+
96+
if ($node instanceof Folder) {
97+
$maybeContents = " and all it's contents";
98+
} else {
99+
$maybeContents = "";
100+
}
101+
$question = new ConfirmationQuestion("Delete " . $node->getPath() . $maybeContents . "? [y/N] ", false);
102+
$deleteConfirmed = $helper->ask($input, $output, $question);
103+
}
104+
105+
if ($deleteConfirmed) {
106+
if ($node->isDeletable()) {
107+
$node->delete();
108+
} else {
109+
$output->writeln("<error>File cannot be deleted, insufficient permissions.</error>");
110+
}
111+
}
112+
113+
return 0;
114+
}
115+
116+
}

apps/files/lib/Command/Get.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
namespace OCA\Files\Command;
25+
26+
27+
use OC\Core\Command\Info\FileUtils;
28+
use OCP\Files\File;
29+
use Symfony\Component\Console\Command\Command;
30+
use Symfony\Component\Console\Input\InputArgument;
31+
use Symfony\Component\Console\Input\InputInterface;
32+
use Symfony\Component\Console\Output\OutputInterface;
33+
34+
class Get extends Command {
35+
private FileUtils $fileUtils;
36+
37+
public function __construct(FileUtils $fileUtils) {
38+
$this->fileUtils = $fileUtils;
39+
parent::__construct();
40+
}
41+
42+
protected function configure(): void {
43+
$this
44+
->setName('files:get')
45+
->setDescription('Get the contents of a file')
46+
->addArgument('file', InputArgument::REQUIRED, "Source file id or Nextcloud path")
47+
->addArgument('output', InputArgument::OPTIONAL, "Target local file to output to, defaults to STDOUT");
48+
}
49+
50+
public function execute(InputInterface $input, OutputInterface $output): int {
51+
$fileInput = $input->getArgument('file');
52+
$outputName = $input->getArgument('output');
53+
$node = $this->fileUtils->getNode($fileInput);
54+
55+
if (!$node) {
56+
$output->writeln("<error>file $fileInput not found</error>");
57+
return 1;
58+
}
59+
60+
if ($node instanceof File) {
61+
$isTTY = stream_isatty(STDOUT);
62+
if ($outputName === null && $isTTY && $node->getMimePart() !== 'text') {
63+
$output->writeln([
64+
"<error>Warning: Binary output can mess up your terminal</error>",
65+
" Use <info>occ files:get $fileInput -</info> to output it to the terminal anyway",
66+
" Or <info>occ files:get $fileInput <FILE></info> to save to a file instead"
67+
]);
68+
return 1;
69+
}
70+
$source = $node->fopen('r');
71+
if (!$source) {
72+
$output->writeln("<error>Failed to open $fileInput for reading</error>");
73+
return 1;
74+
}
75+
$target = ($outputName === null || $outputName === '-') ? STDOUT : fopen($outputName, 'w');
76+
if (!$target) {
77+
$output->writeln("<error>Failed to open $outputName for reading</error>");
78+
return 1;
79+
}
80+
81+
stream_copy_to_stream($source, $target);
82+
return 0;
83+
} else {
84+
$output->writeln("<error>$fileInput is a directory</error>");
85+
return 1;
86+
}
87+
}
88+
89+
}

apps/files/lib/Command/Put.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
namespace OCA\Files\Command;
25+
26+
27+
use OC\Core\Command\Info\FileUtils;
28+
use OCP\Files\File;
29+
use OCP\Files\Folder;
30+
use OCP\Files\IRootFolder;
31+
use Symfony\Component\Console\Command\Command;
32+
use Symfony\Component\Console\Input\InputArgument;
33+
use Symfony\Component\Console\Input\InputInterface;
34+
use Symfony\Component\Console\Output\OutputInterface;
35+
36+
class Put extends Command {
37+
private FileUtils $fileUtils;
38+
private IRootFolder $rootFolder;
39+
40+
public function __construct(FileUtils $fileUtils, IRootFolder $rootFolder) {
41+
$this->fileUtils = $fileUtils;
42+
$this->rootFolder = $rootFolder;
43+
parent::__construct();
44+
}
45+
46+
protected function configure(): void {
47+
$this
48+
->setName('files:put')
49+
->setDescription('Write contents of a file')
50+
->addArgument('input', InputArgument::REQUIRED, "Source local path, use - to read from STDIN")
51+
->addArgument('file', InputArgument::REQUIRED, "Target Nextcloud file path to write to or fileid of existing file");
52+
}
53+
54+
public function execute(InputInterface $input, OutputInterface $output): int {
55+
$fileOutput = $input->getArgument('file');
56+
$inputName = $input->getArgument('input');
57+
$node = $this->fileUtils->getNode($fileOutput);
58+
59+
if ($node instanceof Folder) {
60+
$output->writeln("<error>$fileOutput is a folder</error>");
61+
return 1;
62+
}
63+
if (!$node and is_numeric($fileOutput)) {
64+
$output->writeln("<error>$fileOutput not found</error>");
65+
return 1;
66+
}
67+
68+
$source = ($inputName === null || $inputName === '-') ? STDIN : fopen($inputName, 'r');
69+
if (!$source) {
70+
$output->writeln("<error>Failed to open $inputName</error>");
71+
return 1;
72+
}
73+
if ($node instanceof File) {
74+
$target = $node->fopen('w');
75+
if (!$target) {
76+
$output->writeln("<error>Failed to open $fileOutput</error>");
77+
return 1;
78+
}
79+
stream_copy_to_stream($source, $target);
80+
} else {
81+
$this->rootFolder->newFile($fileOutput, $source);
82+
}
83+
return 0;
84+
}
85+
86+
}

0 commit comments

Comments
 (0)