-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Add command to list orphan objects #51603
base: master
Are you sure you want to change the base?
Changes from all commits
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,79 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\Files\Command\Object; | ||
|
||
use OC\Core\Command\Base; | ||
use OCP\Files\IMimeTypeDetector; | ||
use OCP\Files\ObjectStore\IObjectStoreMetaData; | ||
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 Info extends Base { | ||
public function __construct( | ||
private ObjectUtil $objectUtils, | ||
private IMimeTypeDetector $mimeTypeDetector, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void { | ||
parent::configure(); | ||
$this | ||
->setName('files:object:info') | ||
->setDescription('Get the metadata of an object') | ||
->addArgument('object', InputArgument::REQUIRED, 'Object to get') | ||
->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'); | ||
$objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); | ||
if (!$objectStore) { | ||
return self::FAILURE; | ||
} | ||
|
||
if (!$objectStore instanceof IObjectStoreMetaData) { | ||
$output->writeln('<error>Configured object store does currently not support retrieve metadata</error>'); | ||
return self::FAILURE; | ||
} | ||
|
||
if (!$objectStore->objectExists($object)) { | ||
$output->writeln("<error>Object $object does not exist</error>"); | ||
return self::FAILURE; | ||
} | ||
|
||
try { | ||
$meta = $objectStore->getObjectMetaData($object); | ||
} catch (\Exception $e) { | ||
$msg = $e->getMessage(); | ||
$output->writeln("<error>Failed to read $object from object store: $msg</error>"); | ||
return self::FAILURE; | ||
} | ||
|
||
if ($input->getOption('output') === 'plain' && isset($meta['size'])) { | ||
$meta['size'] = \OC_Helper::humanFileSize($meta['size']); | ||
} | ||
if (isset($meta['mtime'])) { | ||
$meta['mtime'] = $meta['mtime']->format(\DateTimeImmutable::ATOM); | ||
} | ||
if (!isset($meta['mimetype'])) { | ||
$handle = $objectStore->readObject($object); | ||
$head = fread($handle, 8192); | ||
fclose($handle); | ||
$meta['mimetype'] = $this->mimeTypeDetector->detectString($head); | ||
} | ||
|
||
$this->writeArrayInOutputFormat($input, $output, $meta); | ||
|
||
return self::SUCCESS; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\Files\Command\Object; | ||
|
||
use OC\Core\Command\Base; | ||
use OCP\Files\ObjectStore\IObjectStoreMetaData; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
class ListObject extends Base { | ||
private const CHUNK_SIZE = 100; | ||
|
||
public function __construct( | ||
private readonly ObjectUtil $objectUtils, | ||
) { | ||
parent::__construct(); | ||
} | ||
|
||
protected function configure(): void { | ||
parent::configure(); | ||
$this | ||
->setName('files:object:list') | ||
->setDescription('List all objects in the object store') | ||
->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config"); | ||
} | ||
|
||
public function execute(InputInterface $input, OutputInterface $output): int { | ||
$objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); | ||
if (!$objectStore) { | ||
return self::FAILURE; | ||
} | ||
|
||
if (!$objectStore instanceof IObjectStoreMetaData) { | ||
$output->writeln('<error>Configured object store does currently not support listing objects</error>'); | ||
return self::FAILURE; | ||
} | ||
$objects = $objectStore->listObjects(); | ||
$this->objectUtils->writeIteratorToOutput($input, $output, $objects, self::CHUNK_SIZE); | ||
|
||
return self::SUCCESS; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -8,13 +8,15 @@ | |||||||||||
|
||||||||||||
namespace OCA\Files\Command\Object; | ||||||||||||
|
||||||||||||
use OC\Core\Command\Base; | ||||||||||||
use OCP\DB\QueryBuilder\IQueryBuilder; | ||||||||||||
use OCP\Files\ObjectStore\IObjectStore; | ||||||||||||
use OCP\IConfig; | ||||||||||||
use OCP\IDBConnection; | ||||||||||||
use Symfony\Component\Console\Input\InputInterface; | ||||||||||||
use Symfony\Component\Console\Output\OutputInterface; | ||||||||||||
|
||||||||||||
class ObjectUtil { | ||||||||||||
class ObjectUtil extends Base { | ||||||||||||
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. Why is that needed? |
||||||||||||
public function __construct( | ||||||||||||
private IConfig $config, | ||||||||||||
private IDBConnection $connection, | ||||||||||||
|
@@ -91,4 +93,78 @@ public function objectExistsInDb(string $object): int|false { | |||||||||||
|
||||||||||||
return $fileId; | ||||||||||||
} | ||||||||||||
|
||||||||||||
public function writeIteratorToOutput(InputInterface $input, OutputInterface $output, \Iterator $objects, int $chunkSize): void { | ||||||||||||
$outputType = $input->getOption('output'); | ||||||||||||
$humanOutput = $outputType === Base::OUTPUT_FORMAT_PLAIN; | ||||||||||||
$first = true; | ||||||||||||
|
||||||||||||
if (!$humanOutput) { | ||||||||||||
$output->writeln('['); | ||||||||||||
} | ||||||||||||
Comment on lines
+102
to
+104
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. We need a json streaming lib at some point 😛 |
||||||||||||
|
||||||||||||
foreach ($this->chunkIterator($objects, $chunkSize) as $chunk) { | ||||||||||||
if ($outputType === Base::OUTPUT_FORMAT_PLAIN) { | ||||||||||||
$this->outputChunk($input, $output, $chunk); | ||||||||||||
} else { | ||||||||||||
foreach ($chunk as $object) { | ||||||||||||
Comment on lines
+107
to
+110
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. Code architecture is confusing here. |
||||||||||||
if (!$first) { | ||||||||||||
$output->writeln(','); | ||||||||||||
} | ||||||||||||
$row = $this->formatObject($object, $humanOutput); | ||||||||||||
if ($outputType === Base::OUTPUT_FORMAT_JSON_PRETTY) { | ||||||||||||
$output->write(json_encode($row, JSON_PRETTY_PRINT)); | ||||||||||||
} else { | ||||||||||||
$output->write(json_encode($row)); | ||||||||||||
} | ||||||||||||
$first = false; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (!$humanOutput) { | ||||||||||||
$output->writeln("\n]"); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
private function formatObject(array $object, bool $humanOutput): array { | ||||||||||||
$row = array_merge([ | ||||||||||||
'urn' => $object['urn'], | ||||||||||||
], ($object['metadata'] ?? [])); | ||||||||||||
|
||||||||||||
if ($humanOutput && isset($row['size'])) { | ||||||||||||
$row['size'] = \OC_Helper::humanFileSize($row['size']); | ||||||||||||
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
|
||||||||||||
} | ||||||||||||
if (isset($row['mtime'])) { | ||||||||||||
$row['mtime'] = $row['mtime']->format(\DateTimeImmutable::ATOM); | ||||||||||||
} | ||||||||||||
return $row; | ||||||||||||
} | ||||||||||||
|
||||||||||||
private function outputChunk(InputInterface $input, OutputInterface $output, iterable $chunk): void { | ||||||||||||
$result = []; | ||||||||||||
$humanOutput = $input->getOption('output') === 'plain'; | ||||||||||||
|
||||||||||||
foreach ($chunk as $object) { | ||||||||||||
$result[] = $this->formatObject($object, $humanOutput); | ||||||||||||
} | ||||||||||||
$this->writeTableInOutputFormat($input, $output, $result); | ||||||||||||
} | ||||||||||||
|
||||||||||||
public function chunkIterator(\Iterator $iterator, int $count): \Iterator { | ||||||||||||
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. Can you explain/document the point of this chunking? What good does it do to pre-fetch 100 entries in an array rather than fetch them as we go? |
||||||||||||
$chunk = []; | ||||||||||||
|
||||||||||||
for ($i = 0; $iterator->valid(); $i++) { | ||||||||||||
$chunk[] = $iterator->current(); | ||||||||||||
$iterator->next(); | ||||||||||||
if (count($chunk) == $count) { | ||||||||||||
yield $chunk; | ||||||||||||
Comment on lines
+160
to
+161
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
|
||||||||||||
$chunk = []; | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (count($chunk)) { | ||||||||||||
yield $chunk; | ||||||||||||
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
|
||||||||||||
} | ||||||||||||
} | ||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
/** | ||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
namespace OCA\Files\Command\Object; | ||
|
||
use OC\Core\Command\Base; | ||
use OCP\DB\QueryBuilder\IQueryBuilder; | ||
use OCP\Files\ObjectStore\IObjectStoreMetaData; | ||
use OCP\IDBConnection; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
class Orphans extends Base { | ||
private const CHUNK_SIZE = 100; | ||
|
||
private IQueryBuilder $query; | ||
|
||
public function __construct( | ||
private readonly ObjectUtil $objectUtils, | ||
IDBConnection $connection, | ||
) { | ||
parent::__construct(); | ||
|
||
$this->query = $connection->getQueryBuilder(); | ||
$this->query->select('fileid') | ||
->from('filecache') | ||
->where($this->query->expr()->eq('fileid', $this->query->createParameter('file_id'))); | ||
Comment on lines
+30
to
+33
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. Is there really no perf impact on doing that in constructor? |
||
} | ||
|
||
protected function configure(): void { | ||
parent::configure(); | ||
$this | ||
->setName('files:object:orphans') | ||
->setDescription('List all objects in the object store that don\'t have a matching entry in the database') | ||
->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config"); | ||
} | ||
|
||
public function execute(InputInterface $input, OutputInterface $output): int { | ||
$objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output); | ||
if (!$objectStore) { | ||
return self::FAILURE; | ||
} | ||
|
||
if (!$objectStore instanceof IObjectStoreMetaData) { | ||
$output->writeln('<error>Configured object store does currently not support listing objects</error>'); | ||
return self::FAILURE; | ||
} | ||
$prefixLength = strlen('urn:oid:'); | ||
|
||
$objects = $objectStore->listObjects('urn:oid:'); | ||
$objects->rewind(); | ||
$orphans = new \CallbackFilterIterator($objects, function (array $object) use ($prefixLength) { | ||
$fileId = (int)substr($object['urn'], $prefixLength); | ||
return !$this->fileIdInDb($fileId); | ||
}); | ||
$orphans = new \ArrayIterator(iterator_to_array($orphans)); | ||
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. Why? If this is needed it needs a comment explaining. |
||
$this->objectUtils->writeIteratorToOutput($input, $output, $orphans, self::CHUNK_SIZE); | ||
|
||
return self::SUCCESS; | ||
} | ||
|
||
private function fileIdInDb(int $fileId): bool { | ||
$this->query->setParameter('file_id', $fileId, IQueryBuilder::PARAM_INT); | ||
$result = $this->query->executeQuery(); | ||
return $result->fetchOne() !== false; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No more calls to OC_* please 🙏