-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
add utility command for object store objects #38226
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
| /** | ||
| * @copyright Copyright (c) 2023 Robin Appelman <[email protected]> | ||
| * | ||
| * @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 OCA\Files\Command\Object; | ||
|
|
||
| use OCP\DB\QueryBuilder\IQueryBuilder; | ||
| use OCP\IDBConnection; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Helper\QuestionHelper; | ||
| use Symfony\Component\Console\Input\InputArgument; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Input\InputOption; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use Symfony\Component\Console\Question\ConfirmationQuestion; | ||
|
|
||
| class Delete extends Command { | ||
| private ObjectUtil $objectUtils; | ||
|
|
||
| public function __construct(ObjectUtil $objectUtils) { | ||
| $this->objectUtils = $objectUtils; | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| protected function configure(): void { | ||
| $this | ||
| ->setName('files:object:delete') | ||
| ->setDescription('Delete an object from the object store') | ||
| ->addArgument('object', InputArgument::REQUIRED, "Object to delete") | ||
| ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to delete the object from, only required in cases where it can't be determined from the config"); | ||
| } | ||
|
|
||
| public function execute(InputInterface $input, OutputInterface $output): int { | ||
| $object = $input->getArgument('object'); | ||
| $objectStore = $this->objectUtils->getObjectStore($input->getOption("bucket"), $output); | ||
| if (!$objectStore) { | ||
| return -1; | ||
| } | ||
|
|
||
| if ($fileId = $this->objectUtils->objectExistsInDb($object)) { | ||
| $output->writeln("<error>Warning, object $object belongs to an existing file, deleting the object will lead to unexpected behavior if not replaced</error>"); | ||
| $output->writeln(" Note: use <info>occ files:delete $fileId</info> to delete the file cleanly or <info>occ info:file $fileId</info> for more information about the file"); | ||
| $output->writeln(""); | ||
| } | ||
|
|
||
| if (!$objectStore->objectExists($object)) { | ||
| $output->writeln("<error>Object $object does not exist</error>"); | ||
| return -1; | ||
| } | ||
|
|
||
| /** @var QuestionHelper $helper */ | ||
| $helper = $this->getHelper('question'); | ||
| $question = new ConfirmationQuestion("Delete $object? [y/N] ", false); | ||
| if ($helper->ask($input, $output, $question)) { | ||
| $objectStore->deleteObject($object); | ||
| } | ||
| return 0; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
| /** | ||
| * @copyright Copyright (c) 2023 Robin Appelman <[email protected]> | ||
| * | ||
| * @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 OCA\Files\Command\Object; | ||
|
|
||
| use OCP\Files\File; | ||
| 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 Get extends Command { | ||
| private ObjectUtil $objectUtils; | ||
|
|
||
| public function __construct(ObjectUtil $objectUtils) { | ||
| $this->objectUtils = $objectUtils; | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| protected function configure(): void { | ||
| $this | ||
| ->setName('files:object:get') | ||
| ->setDescription('Get the contents of an object') | ||
| ->addArgument('object', InputArgument::REQUIRED, "Object to get") | ||
| ->addArgument('output', InputArgument::REQUIRED, "Target local file to output to, use - for STDOUT") | ||
| ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config"); | ||
| } | ||
|
|
||
| public function execute(InputInterface $input, OutputInterface $output): int { | ||
| $object = $input->getArgument('object'); | ||
| $outputName = $input->getArgument('output'); | ||
| $objectStore = $this->objectUtils->getObjectStore($input->getOption("bucket"), $output); | ||
| if (!$objectStore) { | ||
| return 1; | ||
| } | ||
|
|
||
| if (!$objectStore->objectExists($object)) { | ||
| $output->writeln("<error>Object $object does not exist</error>"); | ||
| return 1; | ||
| } else { | ||
| try { | ||
| $source = $objectStore->readObject($object); | ||
| } catch (\Exception $e) { | ||
| $msg = $e->getMessage(); | ||
| $output->writeln("<error>Failed to read $object from object store: $msg</error>"); | ||
| return 1; | ||
| } | ||
| $target = $outputName === '-' ? STDOUT : fopen($outputName, 'w'); | ||
| if (!$target) { | ||
| $output->writeln("<error>Failed to open $outputName for writing</error>"); | ||
| return 1; | ||
| } | ||
|
|
||
| stream_copy_to_stream($source, $target); | ||
| return 0; | ||
| } | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
| /** | ||
| * @copyright Copyright (c) 2023 Robin Appelman <[email protected]> | ||
| * | ||
| * @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 OCA\Files\Command\Object; | ||
|
|
||
| use OCP\DB\QueryBuilder\IQueryBuilder; | ||
| use OCP\Files\ObjectStore\IObjectStore; | ||
| use OCP\IConfig; | ||
| use OCP\IDBConnection; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
|
|
||
| class ObjectUtil { | ||
| private IConfig $config; | ||
| private IDBConnection $connection; | ||
|
|
||
| public function __construct(IConfig $config, IDBConnection $connection) { | ||
| $this->config = $config; | ||
| $this->connection = $connection; | ||
| } | ||
|
|
||
| private function getObjectStoreConfig(): ?array { | ||
| $config = $this->config->getSystemValue('objectstore_multibucket'); | ||
| if (is_array($config)) { | ||
| $config['multibucket'] = true; | ||
| return $config; | ||
| } | ||
| $config = $this->config->getSystemValue('objectstore'); | ||
| if (is_array($config)) { | ||
| if (!isset($config['multibucket'])) { | ||
| $config['multibucket'] = false; | ||
| } | ||
| return $config; | ||
| } else { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| public function getObjectStore(?string $bucket, OutputInterface $output): ?IObjectStore { | ||
|
||
| $config = $this->getObjectStoreConfig(); | ||
| if (!$config) { | ||
| $output->writeln("<error>Instance is not using primary object store</error>"); | ||
| return null; | ||
| } | ||
| if ($config['multibucket'] && !$bucket) { | ||
| $output->writeln("<error>--bucket option required</error> because <info>multi bucket</info> is enabled."); | ||
| return null; | ||
| } | ||
|
|
||
| if (!isset($config['arguments'])) { | ||
| throw new \Exception("no arguments configured for object store configuration"); | ||
| } | ||
| if (!isset($config['class'])) { | ||
| throw new \Exception("no class configured for object store configuration"); | ||
| } | ||
|
|
||
| if ($bucket) { | ||
| // s3, swift | ||
| $config['arguments']['bucket'] = $bucket; | ||
| // azure | ||
| $config['arguments']['container'] = $bucket; | ||
| } | ||
|
|
||
| $store = new $config['class']($config['arguments']); | ||
| if (!$store instanceof IObjectStore) { | ||
| throw new \Exception("configured object store class is not an object store implementation"); | ||
| } | ||
| return $store; | ||
| } | ||
|
|
||
| /** | ||
| * Check if an object is referenced in the database | ||
| */ | ||
| public function objectExistsInDb(string $object): int|false { | ||
| if (str_starts_with($object, 'urn:oid:')) { | ||
| $fileId = (int)substr($object, strlen('urn:oid:')); | ||
| $query = $this->connection->getQueryBuilder(); | ||
| $query->select('fileid') | ||
| ->from('filecache') | ||
| ->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); | ||
| $result = $query->executeQuery(); | ||
| if ($result->fetchOne() !== false) { | ||
| return $fileId; | ||
icewind1991 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| return false; | ||
| } | ||
| } else { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
| /** | ||
| * @copyright Copyright (c) 2023 Robin Appelman <[email protected]> | ||
| * | ||
| * @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 OCA\Files\Command\Object; | ||
|
|
||
| use OCP\Files\IMimeTypeDetector; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Helper\QuestionHelper; | ||
| use Symfony\Component\Console\Input\InputArgument; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Input\InputOption; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use Symfony\Component\Console\Question\ConfirmationQuestion; | ||
|
|
||
| class Put extends Command { | ||
| private ObjectUtil $objectUtils; | ||
| private IMimeTypeDetector $mimeTypeDetector; | ||
|
|
||
| public function __construct(ObjectUtil $objectUtils, IMimeTypeDetector $mimeTypeDetector) { | ||
| $this->objectUtils = $objectUtils; | ||
| $this->mimeTypeDetector = $mimeTypeDetector; | ||
| parent::__construct(); | ||
| } | ||
|
|
||
| protected function configure(): void { | ||
| $this | ||
| ->setName('files:object:put') | ||
| ->setDescription('Write a file to the object store') | ||
| ->addArgument('input', InputArgument::REQUIRED, "Source local path, use - to read from STDIN") | ||
| ->addArgument('object', InputArgument::REQUIRED, "Object to write") | ||
| ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket where to store the object, only required in cases where it can't be determined from the config");; | ||
| } | ||
|
|
||
| public function execute(InputInterface $input, OutputInterface $output): int { | ||
| $object = $input->getArgument('object'); | ||
| $inputName = (string)$input->getArgument('input'); | ||
| $objectStore = $this->objectUtils->getObjectStore($input->getOption("bucket"), $output); | ||
| if (!$objectStore) { | ||
| return -1; | ||
| } | ||
|
|
||
| if ($fileId = $this->objectUtils->objectExistsInDb($object)) { | ||
| $output->writeln("<error>Warning, object $object belongs to an existing file, overwriting the object contents can lead to unexpected behavior.</error>"); | ||
| $output->writeln("You can use <info>occ files:put $inputName $fileId</info> to write to the file safely."); | ||
| $output->writeln(""); | ||
|
|
||
| /** @var QuestionHelper $helper */ | ||
| $helper = $this->getHelper('question'); | ||
| $question = new ConfirmationQuestion("Write to the object anyway? [y/N] ", false); | ||
| if (!$helper->ask($input, $output, $question)) { | ||
| return -1; | ||
| } | ||
| } | ||
|
|
||
| $source = $inputName === '-' ? STDIN : fopen($inputName, 'r'); | ||
| if (!$source) { | ||
| $output->writeln("<error>Failed to open $inputName</error>"); | ||
| return 1; | ||
| } | ||
| $objectStore->writeObject($object, $source, $this->mimeTypeDetector->detectPath($inputName)); | ||
|
||
| return 0; | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.