migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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;
use OC\Core\Command\Info\FileUtils;
use OCP\Files\Folder;
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 Copy extends Command {
private FileUtils $fileUtils;
public function __construct(FileUtils $fileUtils) {
$this->fileUtils = $fileUtils;
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:copy')
->setDescription('Copy a file or folder')
->addArgument('source', InputArgument::REQUIRED, "Source file id or path")
->addArgument('target', InputArgument::REQUIRED, "Target path")
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for confirmation and don't output any warnings")
->addOption('no-target-directory', 'T', InputOption::VALUE_NONE, "When target path is folder, overwrite the folder instead of copying into the folder");
}
public function execute(InputInterface $input, OutputInterface $output): int {
$sourceInput = $input->getArgument('source');
$targetInput = $input->getArgument('target');
$force = $input->getOption('force');
$noTargetDir = $input->getOption('no-target-directory');
$node = $this->fileUtils->getNode($sourceInput);
$targetNode = $this->fileUtils->getNode($targetInput);
if (!$node) {
$output->writeln("<error>file $sourceInput not found</error>");
return 1;
}
$targetParentPath = dirname(rtrim($targetInput, '/'));
$targetParent = $this->fileUtils->getNode($targetParentPath);
if (!$targetParent) {
$output->writeln("<error>Target parent path $targetParentPath doesn't exist</error>");
return 1;
}
$wouldRequireDelete = false;
if ($targetNode) {
if (!$targetNode->isUpdateable()) {
$output->writeln("<error>$targetInput isn't writable</error>");
return 1;
}
if ($targetNode instanceof Folder) {
if ($noTargetDir) {
if (!$force) {
$output->writeln("Warning: <info>$sourceInput</info> is a file, but <info>$targetInput</info> is a folder");
}
$wouldRequireDelete = true;
} else {
$targetInput = $targetNode->getFullPath($node->getName());
$targetNode = $this->fileUtils->getNode($targetInput);
}
} else {
if ($node instanceof Folder) {
if (!$force) {
$output->writeln("Warning: <info>$sourceInput</info> is a folder, but <info>$targetInput</info> is a file");
}
$wouldRequireDelete = true;
}
}
if ($wouldRequireDelete && $targetNode->getInternalPath() === '') {
$output->writeln("<error>Mount root can't be overwritten with a different type</error>");
return 1;
}
if ($wouldRequireDelete && !$targetNode->isDeletable()) {
$output->writeln("<error>$targetInput can't be deleted to be replaced with $sourceInput</error>");
return 1;
}
if (!$force && $targetNode) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("<info>" . $targetInput . "</info> already exists, overwrite? [y/N] ", false);
if (!$helper->ask($input, $output, $question)) {
return 1;
}
}
}
if ($wouldRequireDelete && $targetNode) {
$targetNode->delete();
}
$node->copy($targetInput);
return 0;
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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;
use OC\Core\Command\Info\FileUtils;
use OCA\Files_Sharing\SharedStorage;
use OCP\Files\Folder;
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 {
public function __construct(
private FileUtils $fileUtils,
) {
parent::__construct();
}
protected function configure(): void {
$this
->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");
}
public function execute(InputInterface $input, OutputInterface $output): int {
$fileInput = $input->getArgument('file');
$inputIsId = is_numeric($fileInput);
$force = $input->getOption('force');
$node = $this->fileUtils->getNode($fileInput);
if (!$node) {
$output->writeln("<error>file $fileInput not found</error>");
return self::FAILURE;
}
$deleteConfirmed = $force;
if (!$deleteConfirmed) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$storage = $node->getStorage();
if (!$inputIsId && $storage->instanceOfStorage(SharedStorage::class) && $node->getInternalPath() === '') {
/** @var SharedStorage $storage */
[,$user] = explode('/', $fileInput, 3);
$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);
if ($helper->ask($input, $output, $question)) {
$storage->unshareStorage();
return self::SUCCESS;
} else {
$node = $storage->getShare()->getNode();
$output->writeln("");
}
}
$filesByUsers = $this->fileUtils->getFilesByUser($node);
if (count($filesByUsers) > 1) {
$output->writeln("Warning: the provided file is accessible by more than one user");
$output->writeln(" all of the following users will lose access to the file when deleted:");
$output->writeln("");
foreach ($filesByUsers as $user => $filesByUser) {
$output->writeln($user . ":");
foreach($filesByUser as $file) {
$output->writeln(" - " . $file->getPath());
}
}
$output->writeln("");
}
if ($node instanceof Folder) {
$maybeContents = " and all it's contents";
} else {
$maybeContents = "";
}
$question = new ConfirmationQuestion("Delete " . $node->getPath() . $maybeContents . "? [y/N] ", false);
$deleteConfirmed = $helper->ask($input, $output, $question);
}
if ($deleteConfirmed) {
if ($node->isDeletable()) {
$node->delete();
} else {
$output->writeln("<error>File cannot be deleted, insufficient permissions.</error>");
}
}
return self::SUCCESS;
}
}
@@ -0,0 +1,112 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @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 OCP\IDBConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Delete all file entries that have no matching entries in the storage table.
*/
class DeleteOrphanedFiles extends Command {
public const CHUNK_SIZE = 200;
public function __construct(
protected IDBConnection $connection,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:cleanup')
->setDescription('cleanup filecache');
}
public function execute(InputInterface $input, OutputInterface $output): int {
$deletedEntries = 0;
$query = $this->connection->getQueryBuilder();
$query->select('fc.fileid')
->from('filecache', 'fc')
->where($query->expr()->isNull('s.numeric_id'))
->leftJoin('fc', 'storages', 's', $query->expr()->eq('fc.storage', 's.numeric_id'))
->setMaxResults(self::CHUNK_SIZE);
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete('filecache')
->where($deleteQuery->expr()->eq('fileid', $deleteQuery->createParameter('objectid')));
$deletedInLastChunk = self::CHUNK_SIZE;
while ($deletedInLastChunk === self::CHUNK_SIZE) {
$deletedInLastChunk = 0;
$result = $query->execute();
while ($row = $result->fetch()) {
$deletedInLastChunk++;
$deletedEntries += $deleteQuery->setParameter('objectid', (int) $row['fileid'])
->execute();
}
$result->closeCursor();
}
$output->writeln("$deletedEntries orphaned file cache entries deleted");
$deletedMounts = $this->cleanupOrphanedMounts();
$output->writeln("$deletedMounts orphaned mount entries deleted");
return self::SUCCESS;
}
private function cleanupOrphanedMounts(): int {
$deletedEntries = 0;
$query = $this->connection->getQueryBuilder();
$query->select('m.storage_id')
->from('mounts', 'm')
->where($query->expr()->isNull('s.numeric_id'))
->leftJoin('m', 'storages', 's', $query->expr()->eq('m.storage_id', 's.numeric_id'))
->groupBy('storage_id')
->setMaxResults(self::CHUNK_SIZE);
$deleteQuery = $this->connection->getQueryBuilder();
$deleteQuery->delete('mounts')
->where($deleteQuery->expr()->eq('storage_id', $deleteQuery->createParameter('storageid')));
$deletedInLastChunk = self::CHUNK_SIZE;
while ($deletedInLastChunk === self::CHUNK_SIZE) {
$deletedInLastChunk = 0;
$result = $query->execute();
while ($row = $result->fetch()) {
$deletedInLastChunk++;
$deletedEntries += $deleteQuery->setParameter('storageid', (int) $row['storage_id'])
->execute();
}
$result->closeCursor();
}
return $deletedEntries;
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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;
use OC\Core\Command\Info\FileUtils;
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\Output\OutputInterface;
class Get extends Command {
public function __construct(
private FileUtils $fileUtils,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:get')
->setDescription('Get the contents of a file')
->addArgument('file', InputArgument::REQUIRED, "Source file id or Nextcloud path")
->addArgument('output', InputArgument::OPTIONAL, "Target local file to output to, defaults to STDOUT");
}
public function execute(InputInterface $input, OutputInterface $output): int {
$fileInput = $input->getArgument('file');
$outputName = $input->getArgument('output');
$node = $this->fileUtils->getNode($fileInput);
if (!$node) {
$output->writeln("<error>file $fileInput not found</error>");
return self::FAILURE;
}
if (!($node instanceof File)) {
$output->writeln("<error>$fileInput is a directory</error>");
return self::FAILURE;
}
$isTTY = stream_isatty(STDOUT);
if ($outputName === null && $isTTY && $node->getMimePart() !== 'text') {
$output->writeln([
"<error>Warning: Binary output can mess up your terminal</error>",
" Use <info>occ files:get $fileInput -</info> to output it to the terminal anyway",
" Or <info>occ files:get $fileInput <FILE></info> to save to a file instead"
]);
return self::FAILURE;
}
$source = $node->fopen('r');
if (!$source) {
$output->writeln("<error>Failed to open $fileInput for reading</error>");
return self::FAILURE;
}
$target = ($outputName === null || $outputName === '-') ? STDOUT : fopen($outputName, 'w');
if (!$target) {
$output->writeln("<error>Failed to open $outputName for reading</error>");
return self::FAILURE;
}
stream_copy_to_stream($source, $target);
return self::SUCCESS;
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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;
use OC\Core\Command\Info\FileUtils;
use OCP\Files\File;
use OCP\Files\Folder;
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 Move extends Command {
private FileUtils $fileUtils;
public function __construct(FileUtils $fileUtils) {
$this->fileUtils = $fileUtils;
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:move')
->setDescription('Move a file or folder')
->addArgument('source', InputArgument::REQUIRED, "Source file id or path")
->addArgument('target', InputArgument::REQUIRED, "Target path")
->addOption('force', 'f', InputOption::VALUE_NONE, "Don't ask for configuration and don't output any warnings");
}
public function execute(InputInterface $input, OutputInterface $output): int {
$sourceInput = $input->getArgument('source');
$targetInput = $input->getArgument('target');
$force = $input->getOption('force');
$node = $this->fileUtils->getNode($sourceInput);
$targetNode = $this->fileUtils->getNode($targetInput);
if (!$node) {
$output->writeln("<error>file $sourceInput not found</error>");
return 1;
}
$targetParentPath = dirname(rtrim($targetInput, '/'));
$targetParent = $this->fileUtils->getNode($targetParentPath);
if (!$targetParent) {
$output->writeln("<error>Target parent path $targetParentPath doesn't exist</error>");
return 1;
}
$wouldRequireDelete = false;
if ($targetNode) {
if (!$targetNode->isUpdateable()) {
$output->writeln("<error>$targetInput already exists and isn't writable</error>");
return 1;
}
if ($node instanceof Folder && $targetNode instanceof File) {
$output->writeln("Warning: <info>$sourceInput</info> is a folder, but <info>$targetInput</info> is a file");
$wouldRequireDelete = true;
}
if ($node instanceof File && $targetNode instanceof Folder) {
$output->writeln("Warning: <info>$sourceInput</info> is a file, but <info>$targetInput</info> is a folder");
$wouldRequireDelete = true;
}
if ($wouldRequireDelete && $targetNode->getInternalPath() === '') {
$output->writeln("<error>Mount root can't be overwritten with a different type</error>");
return 1;
}
if ($wouldRequireDelete && !$targetNode->isDeletable()) {
$output->writeln("<error>$targetInput can't be deleted to be replaced with $sourceInput</error>");
return 1;
}
if (!$force) {
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion("<info>" . $targetInput . "</info> already exists, overwrite? [y/N] ", false);
if (!$helper->ask($input, $output, $question)) {
return 1;
}
}
}
if ($wouldRequireDelete && $targetNode) {
$targetNode->delete();
}
$node->move($targetInput);
return 0;
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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 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 {
public function __construct(
private ObjectUtil $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 self::SUCCESS;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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 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 {
public function __construct(
private ObjectUtil $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 self::FAILURE;
}
if (!$objectStore->objectExists($object)) {
$output->writeln("<error>Object $object does not exist</error>");
return self::FAILURE;
}
try {
$source = $objectStore->readObject($object);
} catch (\Exception $e) {
$msg = $e->getMessage();
$output->writeln("<error>Failed to read $object from object store: $msg</error>");
return self::FAILURE;
}
$target = $outputName === '-' ? STDOUT : fopen($outputName, 'w');
if (!$target) {
$output->writeln("<error>Failed to open $outputName for writing</error>");
return self::FAILURE;
}
stream_copy_to_stream($source, $target);
return self::SUCCESS;
}
}
@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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 {
public function __construct(
private IConfig $config,
private IDBConnection $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;
}
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:')) {
return false;
}
$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 false;
}
return $fileId;
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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 {
public function __construct(
private ObjectUtil $objectUtils,
private IMimeTypeDetector $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 self::FAILURE;
}
$objectStore->writeObject($object, $source, $this->mimeTypeDetector->detectPath($inputName));
return self::SUCCESS;
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.nl>
*
* @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;
use OC\Core\Command\Info\FileUtils;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Put extends Command {
public function __construct(
private FileUtils $fileUtils,
private IRootFolder $rootFolder,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:put')
->setDescription('Write contents of a file')
->addArgument('input', InputArgument::REQUIRED, "Source local path, use - to read from STDIN")
->addArgument('file', InputArgument::REQUIRED, "Target Nextcloud file path to write to or fileid of existing file");
}
public function execute(InputInterface $input, OutputInterface $output): int {
$fileOutput = $input->getArgument('file');
$inputName = $input->getArgument('input');
$node = $this->fileUtils->getNode($fileOutput);
if ($node instanceof Folder) {
$output->writeln("<error>$fileOutput is a folder</error>");
return self::FAILURE;
}
if (!$node and is_numeric($fileOutput)) {
$output->writeln("<error>$fileOutput not found</error>");
return self::FAILURE;
}
$source = ($inputName === null || $inputName === '-') ? STDIN : fopen($inputName, 'r');
if (!$source) {
$output->writeln("<error>Failed to open $inputName</error>");
return self::FAILURE;
}
if ($node instanceof File) {
$target = $node->fopen('w');
if (!$target) {
$output->writeln("<error>Failed to open $fileOutput</error>");
return self::FAILURE;
}
stream_copy_to_stream($source, $target);
} else {
$this->rootFolder->newFile($fileOutput, $source);
}
return self::SUCCESS;
}
}
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Robin Appelman <robin@icewind.nl>
*
* @author Robin Appelman <robin@icewind.nl>
*
* @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;
use OCP\IDBConnection;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RepairTree extends Command {
public const CHUNK_SIZE = 200;
public function __construct(
protected IDBConnection $connection,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:repair-tree')
->setDescription('Try and repair malformed filesystem tree structures')
->addOption('dry-run');
}
public function execute(InputInterface $input, OutputInterface $output): int {
$rows = $this->findBrokenTreeBits();
$fix = !$input->getOption('dry-run');
$output->writeln("Found " . count($rows) . " file entries with an invalid path");
if ($fix) {
$this->connection->beginTransaction();
}
$query = $this->connection->getQueryBuilder();
$query->update('filecache')
->set('path', $query->createParameter('path'))
->set('path_hash', $query->func()->md5($query->createParameter('path')))
->set('storage', $query->createParameter('storage'))
->where($query->expr()->eq('fileid', $query->createParameter('fileid')));
foreach ($rows as $row) {
$output->writeln("Path of file {$row['fileid']} is {$row['path']} but should be {$row['parent_path']}/{$row['name']} based on its parent", OutputInterface::VERBOSITY_VERBOSE);
if ($fix) {
$fileId = $this->getFileId((int)$row['parent_storage'], $row['parent_path'] . '/' . $row['name']);
if ($fileId > 0) {
$output->writeln("Cache entry has already be recreated with id $fileId, deleting instead");
$this->deleteById((int)$row['fileid']);
} else {
$query->setParameters([
'fileid' => $row['fileid'],
'path' => $row['parent_path'] . '/' . $row['name'],
'storage' => $row['parent_storage'],
]);
$query->execute();
}
}
}
if ($fix) {
$this->connection->commit();
}
return self::SUCCESS;
}
private function getFileId(int $storage, string $path) {
$query = $this->connection->getQueryBuilder();
$query->select('fileid')
->from('filecache')
->where($query->expr()->eq('storage', $query->createNamedParameter($storage)))
->andWhere($query->expr()->eq('path_hash', $query->createNamedParameter(md5($path))));
return $query->execute()->fetch(\PDO::FETCH_COLUMN);
}
private function deleteById(int $fileId): void {
$query = $this->connection->getQueryBuilder();
$query->delete('filecache')
->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId)));
$query->execute();
}
private function findBrokenTreeBits(): array {
$query = $this->connection->getQueryBuilder();
$query->select('f.fileid', 'f.path', 'f.parent', 'f.name')
->selectAlias('p.path', 'parent_path')
->selectAlias('p.storage', 'parent_storage')
->from('filecache', 'f')
->innerJoin('f', 'filecache', 'p', $query->expr()->eq('f.parent', 'p.fileid'))
->where($query->expr()->orX(
$query->expr()->andX(
$query->expr()->neq('p.path_hash', $query->createNamedParameter(md5(''))),
$query->expr()->neq('f.path', $query->func()->concat('p.path', $query->func()->concat($query->createNamedParameter('/'), 'f.name')))
),
$query->expr()->andX(
$query->expr()->eq('p.path_hash', $query->createNamedParameter(md5(''))),
$query->expr()->neq('f.path', 'f.name')
),
$query->expr()->neq('f.storage', 'p.storage')
));
return $query->execute()->fetchAll();
}
}
@@ -0,0 +1,355 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bart Visscher <bartv@thisnet.nl>
* @author Blaok <i@blaok.me>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Joel S <joel.devbox@protonmail.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author martin.mattel@diemattels.at <martin.mattel@diemattels.at>
* @author Maxence Lange <maxence@artificial-owl.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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\DB\Connection;
use OC\DB\ConnectionAdapter;
use OC\FilesMetadata\FilesMetadataManager;
use OC\ForbiddenException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Events\FileCacheUpdated;
use OCP\Files\Events\NodeAddedToCache;
use OCP\Files\Events\NodeRemovedFromCache;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\IFilesMetadataManager;
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\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Scan extends Base {
protected float $execTime = 0;
protected int $foldersCounter = 0;
protected int $filesCounter = 0;
protected int $errorsCounter = 0;
protected int $newCounter = 0;
protected int $updatedCounter = 0;
protected int $removedCounter = 0;
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:scan')
->setDescription('rescan filesystem')
->addArgument(
'user_id',
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
'will rescan all files of the given user(s)'
)
->addOption(
'path',
'p',
InputArgument::OPTIONAL,
'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
)
->addOption(
'generate-metadata',
null,
InputOption::VALUE_OPTIONAL,
'Generate metadata for all scanned files; if specified only generate for named value',
''
)
->addOption(
'all',
null,
InputOption::VALUE_NONE,
'will rescan all files of all known users'
)->addOption(
'unscanned',
null,
InputOption::VALUE_NONE,
'only scan files which are marked as not fully scanned'
)->addOption(
'shallow',
null,
InputOption::VALUE_NONE,
'do not scan folders recursively'
)->addOption(
'home-only',
null,
InputOption::VALUE_NONE,
'only scan the home storage, ignoring any mounted external storage or share'
);
}
protected function scanFiles(string $user, string $path, ?string $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
$connection = $this->reconnectToDatabase($output);
$scanner = new \OC\Files\Utils\Scanner(
$user,
new ConnectionAdapter($connection),
\OC::$server->get(IEventDispatcher::class),
\OC::$server->get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata) {
$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->filesCounter;
$this->abortIfInterrupted();
if ($scanMetadata !== null) {
$node = $this->rootFolder->get($path);
$this->filesMetadataManager->refreshMetadata(
$node,
($scanMetadata !== '') ? IFilesMetadataManager::PROCESS_NAMED : IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND,
$scanMetadata
);
}
});
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
$output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->foldersCounter;
$this->abortIfInterrupted();
});
$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
++$this->errorsCounter;
});
$scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
++$this->errorsCounter;
});
$this->eventDispatcher->addListener(NodeAddedToCache::class, function () {
++$this->newCounter;
});
$this->eventDispatcher->addListener(FileCacheUpdated::class, function () {
++$this->updatedCounter;
});
$this->eventDispatcher->addListener(NodeRemovedFromCache::class, function () {
++$this->removedCounter;
});
try {
if ($backgroundScan) {
$scanner->backgroundScan($path);
} else {
$scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
}
} 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 scan command only as the user the web server runs as');
++$this->errorsCounter;
} 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>');
++$this->errorsCounter;
} catch (\Exception $e) {
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
++$this->errorsCounter;
}
}
public function filterHomeMount(IMountPoint $mountPoint): bool {
// any mountpoint inside '/$user/files/'
return substr_count($mountPoint->getMountPoint(), '/') <= 3;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$inputPath = $input->getOption('path');
if ($inputPath) {
$inputPath = '/' . trim($inputPath, '/');
[, $user,] = explode('/', $inputPath, 3);
$users = [$user];
} elseif ($input->getOption('all')) {
$users = $this->userManager->search('');
} else {
$users = $input->getArgument('user_id');
}
# check quantity of users to be process and show it on the command line
$users_total = count($users);
if ($users_total === 0) {
$output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
return self::FAILURE;
}
$this->initTools($output);
// getOption() logic on VALUE_OPTIONAL
$metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
if ($input->getOption('generate-metadata') !== '') {
$metadata = $input->getOption('generate-metadata') ?? '';
}
$user_count = 0;
foreach ($users as $user) {
if (is_object($user)) {
$user = $user->getUID();
}
$path = $inputPath ?: '/' . $user;
++$user_count;
if ($this->userManager->userExists($user)) {
$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
$this->scanFiles($user, $path, $metadata, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
} else {
$output->writeln("<error>Unknown user $user_count $user</error>");
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
}
try {
$this->abortIfInterrupted();
} catch (InterruptedException $e) {
break;
}
}
$this->presentStats($output);
return self::SUCCESS;
}
/**
* Initialises some useful tools for the Command
*/
protected function initTools(OutputInterface $output): void {
// Start the timer
$this->execTime = -microtime(true);
// 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 scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>', OutputInterface::VERBOSITY_VERY_VERBOSE);
++$this->errorsCounter;
return true;
}
protected function presentStats(OutputInterface $output): void {
// Stop the timer
$this->execTime += microtime(true);
$this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
$headers = [
'Folders',
'Files',
'New',
'Updated',
'Removed',
'Errors',
'Elapsed time',
];
$niceDate = $this->formatExecTime();
$rows = [
$this->foldersCounter,
$this->filesCounter,
$this->newCounter,
$this->updatedCounter,
$this->removedCounter,
$this->errorsCounter,
$niceDate,
];
$table = new Table($output);
$table
->setHeaders($headers)
->setRows([$rows]);
$table->render();
}
/**
* Formats microtime into a human-readable format
*/
protected function formatExecTime(): string {
$secs = (int)round($this->execTime);
# convert seconds into HH:MM:SS form
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
}
protected function reconnectToDatabase(OutputInterface $output): Connection {
/** @var Connection $connection */
$connection = \OC::$server->get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
}
while (!$connection->isConnected()) {
try {
$connection->connect();
} catch (\Exception $ex) {
$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
sleep(60);
}
}
return $connection;
}
}
@@ -0,0 +1,267 @@
<?php
/**
* @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author J0WI <J0WI@users.noreply.github.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Joel S <joel.devbox@protonmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Erik Wouters <6179932+EWouters@users.noreply.github.com>
*
* @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;
use OC\Core\Command\Base;
use OC\Core\Command\InterruptedException;
use OC\DB\Connection;
use OC\DB\ConnectionAdapter;
use OC\ForbiddenException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
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 ScanAppData extends Base {
protected float $execTime = 0;
protected int $foldersCounter = 0;
protected int $filesCounter = 0;
public function __construct(
protected IRootFolder $rootFolder,
protected IConfig $config,
) {
parent::__construct();
}
protected function configure(): void {
parent::configure();
$this
->setName('files:scan-app-data')
->setDescription('rescan the AppData folder');
$this->addArgument('folder', InputArgument::OPTIONAL, 'The appdata subfolder to scan', '');
}
protected function scanFiles(OutputInterface $output, string $folder): int {
try {
/** @var \OCP\Files\Folder $appData */
$appData = $this->getAppDataFolder();
} catch (NotFoundException $e) {
$output->writeln('<error>NoAppData folder found</error>');
return self::FAILURE;
}
if ($folder !== '') {
try {
$appData = $appData->get($folder);
} catch (NotFoundException $e) {
$output->writeln('<error>Could not find folder: ' . $folder . '</error>');
return self::FAILURE;
}
}
$connection = $this->reconnectToDatabase($output);
$scanner = new \OC\Files\Utils\Scanner(
null,
new ConnectionAdapter($connection),
\OC::$server->query(IEventDispatcher::class),
\OC::$server->get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
$output->writeln("\tFile <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->filesCounter;
$this->abortIfInterrupted();
});
$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
$output->writeln("\tFolder <info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
++$this->foldersCounter;
$this->abortIfInterrupted();
});
$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
});
$scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output) {
$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
});
try {
$scanner->scan($appData->getPath());
} catch (ForbiddenException $e) {
$output->writeln('<error>Storage not writable</error>');
$output->writeln('<info>Make sure you\'re running the scan command only as the user the web server runs as</info>');
return self::FAILURE;
} catch (InterruptedException $e) {
# exit the function if ctrl-c has been pressed
$output->writeln('<info>Interrupted by user</info>');
return self::FAILURE;
} catch (NotFoundException $e) {
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
return self::FAILURE;
} catch (\Exception $e) {
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
return self::FAILURE;
}
return self::SUCCESS;
}
protected function execute(InputInterface $input, OutputInterface $output): int {
# restrict the verbosity level to VERBOSITY_VERBOSE
if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
}
$output->writeln('Scanning AppData for files');
$output->writeln('');
$folder = $input->getArgument('folder');
$this->initTools();
$exitCode = $this->scanFiles($output, $folder);
if ($exitCode === 0) {
$this->presentStats($output);
}
return $exitCode;
}
/**
* Initialises some useful tools for the Command
*/
protected function initTools(): void {
// Start the timer
$this->execTime = -microtime(true);
// Convert PHP errors to exceptions
set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
}
/**
* Processes PHP errors as exceptions in order to be able to keep track of problems
*
* @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
*
* @throws \ErrorException
*/
public function exceptionErrorHandler($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
// This error code is not included in error_reporting
return;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
}
protected function presentStats(OutputInterface $output): void {
// Stop the timer
$this->execTime += microtime(true);
$headers = [
'Folders', 'Files', 'Elapsed time'
];
$this->showSummary($headers, null, $output);
}
/**
* Shows a summary of operations
*
* @param string[] $headers
* @param string[] $rows
*/
protected function showSummary($headers, $rows, OutputInterface $output): void {
$niceDate = $this->formatExecTime();
if (!$rows) {
$rows = [
$this->foldersCounter,
$this->filesCounter,
$niceDate,
];
}
$table = new Table($output);
$table
->setHeaders($headers)
->setRows([$rows]);
$table->render();
}
/**
* Formats microtime into a human-readable format
*/
protected function formatExecTime(): string {
$secs = round($this->execTime);
# convert seconds into HH:MM:SS form
return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), (int)$secs % 60);
}
protected function reconnectToDatabase(OutputInterface $output): Connection {
/** @var Connection $connection*/
$connection = \OC::$server->get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
}
while (!$connection->isConnected()) {
try {
$connection->connect();
} catch (\Exception $ex) {
$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
sleep(60);
}
}
return $connection;
}
/**
* @throws NotFoundException
*/
private function getAppDataFolder(): Node {
$instanceId = $this->config->getSystemValue('instanceid', null);
if ($instanceId === null) {
throw new NotFoundException();
}
return $this->rootFolder->get('appdata_'.$instanceId);
}
}
@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Carla Schroder <carla@owncloud.com>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Sujith Haridasan <sujith.h@gmail.com>
* @author Sujith H <sharidasan@owncloud.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tobia De Koninck <LEDfan@users.noreply.github.com>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @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 OCA\Files\Exception\TransferOwnershipException;
use OCA\Files\Service\OwnershipTransferService;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserManager;
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 TransferOwnership extends Command {
public function __construct(
private IUserManager $userManager,
private OwnershipTransferService $transferService,
private IConfig $config,
) {
parent::__construct();
}
protected function configure(): void {
$this
->setName('files:transfer-ownership')
->setDescription('All files and folders are moved to another user - outgoing shares and incoming user file shares (optionally) are moved as well.')
->addArgument(
'source-user',
InputArgument::REQUIRED,
'owner of files which shall be moved'
)
->addArgument(
'destination-user',
InputArgument::REQUIRED,
'user who will be the new owner of the files'
)
->addOption(
'path',
null,
InputOption::VALUE_REQUIRED,
'selectively provide the path to transfer. For example --path="folder_name"',
''
)->addOption(
'move',
null,
InputOption::VALUE_NONE,
'move data from source user to root directory of destination user, which must be empty'
)->addOption(
'transfer-incoming-shares',
null,
InputOption::VALUE_OPTIONAL,
'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)',
'2'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int {
/**
* Check if source and destination users are same. If they are same then just ignore the transfer.
*/
if ($input->getArgument(('source-user')) === $input->getArgument('destination-user')) {
$output->writeln("<error>Ownership can't be transferred when Source and Destination users are the same user. Please check your input.</error>");
return self::FAILURE;
}
$sourceUserObject = $this->userManager->get($input->getArgument('source-user'));
$destinationUserObject = $this->userManager->get($input->getArgument('destination-user'));
if (!$sourceUserObject instanceof IUser) {
$output->writeln("<error>Unknown source user " . $input->getArgument('source-user') . "</error>");
return self::FAILURE;
}
if (!$destinationUserObject instanceof IUser) {
$output->writeln("<error>Unknown destination user " . $input->getArgument('destination-user') . "</error>");
return self::FAILURE;
}
try {
$includeIncomingArgument = $input->getOption('transfer-incoming-shares');
switch ($includeIncomingArgument) {
case '0':
$includeIncoming = false;
break;
case '1':
$includeIncoming = true;
break;
case '2':
$includeIncoming = $this->config->getSystemValue('transferIncomingShares', false);
if (gettype($includeIncoming) !== 'boolean') {
$output->writeln("<error> config.php: 'transfer-incoming-shares': wrong usage. Transfer aborted.</error>");
return self::FAILURE;
}
break;
default:
$output->writeln("<error>Option --transfer-incoming-shares: wrong usage. Transfer aborted.</error>");
return self::FAILURE;
}
$this->transferService->transfer(
$sourceUserObject,
$destinationUserObject,
ltrim($input->getOption('path'), '/'),
$output,
$input->getOption('move') === true,
false,
$includeIncoming
);
} catch (TransferOwnershipException $e) {
$output->writeln("<error>" . $e->getMessage() . "</error>");
return $e->getCode() !== 0 ? $e->getCode() : self::FAILURE;
}
return self::SUCCESS;
}
}