summaryrefslogtreecommitdiffstats
path: root/apps/files/lib/Command
diff options
context:
space:
mode:
authorJoas Schilling <nickvergessen@gmx.de>2016-05-12 12:07:06 +0200
committerThomas Müller <DeepDiver1975@users.noreply.github.com>2016-05-12 12:07:06 +0200
commitb34bacd0718fa24c67a8ef0aa6f3b824a9b525bb (patch)
tree904bda1263850905c2c8164f4f1367d8c7bc9d46 /apps/files/lib/Command
parenteea98f1d74daf2a20c6b08b9df743f0478c48103 (diff)
downloadnextcloud-server-b34bacd0718fa24c67a8ef0aa6f3b824a9b525bb.tar.gz
nextcloud-server-b34bacd0718fa24c67a8ef0aa6f3b824a9b525bb.zip
Move Files app to PSR-4 (#24569)
* Move lib/ of Files app to PSR-4 * Move tests to PSR-4
Diffstat (limited to 'apps/files/lib/Command')
-rw-r--r--apps/files/lib/Command/DeleteOrphanedFiles.php84
-rw-r--r--apps/files/lib/Command/Scan.php310
-rw-r--r--apps/files/lib/Command/TransferOwnership.php240
3 files changed, 634 insertions, 0 deletions
diff --git a/apps/files/lib/Command/DeleteOrphanedFiles.php b/apps/files/lib/Command/DeleteOrphanedFiles.php
new file mode 100644
index 00000000000..91043471ce5
--- /dev/null
+++ b/apps/files/lib/Command/DeleteOrphanedFiles.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @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 Doctrine\DBAL\Platforms\PostgreSqlPlatform;
+use Doctrine\DBAL\Platforms\SqlitePlatform;
+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 {
+
+ const CHUNK_SIZE = 200;
+
+ /**
+ * @var IDBConnection
+ */
+ protected $connection;
+
+ public function __construct(IDBConnection $connection) {
+ $this->connection = $connection;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('files:cleanup')
+ ->setDescription('cleanup filecache');
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output) {
+ $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");
+ }
+
+}
diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php
new file mode 100644
index 00000000000..1ae04c585bb
--- /dev/null
+++ b/apps/files/lib/Command/Scan.php
@@ -0,0 +1,310 @@
+<?php
+/**
+ * @author Bart Visscher <bartv@thisnet.nl>
+ * @author Jörn Friedrich Dreyer <jfd@butonic.de>
+ * @author martin.mattel@diemattels.at <martin.mattel@diemattels.at>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Robin Appelman <icewind@owncloud.com>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @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\ForbiddenException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUserManager;
+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\Helper\Table;
+
+class Scan extends Base {
+
+ /** @var IUserManager $userManager */
+ private $userManager;
+ /** @var float */
+ protected $execTime = 0;
+ /** @var int */
+ protected $foldersCounter = 0;
+ /** @var int */
+ protected $filesCounter = 0;
+
+ public function __construct(IUserManager $userManager) {
+ $this->userManager = $userManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ 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(
+ 'quiet',
+ 'q',
+ InputOption::VALUE_NONE,
+ 'suppress any output'
+ )
+ ->addOption(
+ 'verbose',
+ '-v|vv|vvv',
+ InputOption::VALUE_NONE,
+ 'verbose the output'
+ )
+ ->addOption(
+ 'all',
+ null,
+ InputOption::VALUE_NONE,
+ 'will rescan all files of all known users'
+ );
+ }
+
+ public function checkScanWarning($fullPath, OutputInterface $output) {
+ $normalizedPath = basename(\OC\Files\Filesystem::normalizePath($fullPath));
+ $path = basename($fullPath);
+
+ if ($normalizedPath !== $path) {
+ $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
+ }
+ }
+
+ protected function scanFiles($user, $path, $verbose, OutputInterface $output) {
+ $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger());
+ # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
+ # printout and count
+ if ($verbose) {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function ($path) use ($output) {
+ $output->writeln("\tFile <info>$path</info>");
+ $this->filesCounter += 1;
+ if ($this->hasBeenInterrupted()) {
+ throw new \Exception('ctrl-c');
+ }
+ });
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output) {
+ $output->writeln("\tFolder <info>$path</info>");
+ $this->foldersCounter += 1;
+ if ($this->hasBeenInterrupted()) {
+ throw new \Exception('ctrl-c');
+ }
+ });
+ $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output) {
+ $output->writeln("Error while scanning, storage not available (" . $e->getMessage() . ")");
+ });
+ # count only
+ } else {
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function () use ($output) {
+ $this->filesCounter += 1;
+ if ($this->hasBeenInterrupted()) {
+ throw new \Exception('ctrl-c');
+ }
+ });
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function () use ($output) {
+ $this->foldersCounter += 1;
+ if ($this->hasBeenInterrupted()) {
+ throw new \Exception('ctrl-c');
+ }
+ });
+ }
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function($path) use ($output) {
+ $this->checkScanWarning($path, $output);
+ });
+ $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function($path) use ($output) {
+ $this->checkScanWarning($path, $output);
+ });
+
+ try {
+ $scanner->scan($path);
+ } catch (ForbiddenException $e) {
+ $output->writeln("<error>Home storage for user $user not writable</error>");
+ $output->writeln("Make sure you're running the scan command only as the user the web server runs as");
+ } catch (\Exception $e) {
+ if ($e->getMessage() !== 'ctrl-c') {
+ $output->writeln('<error>Exception while scanning: ' . $e->getMessage() . "\n" . $e->getTraceAsString() . '</error>');
+ }
+ return;
+ }
+ }
+
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $inputPath = $input->getOption('path');
+ if ($inputPath) {
+ $inputPath = '/' . trim($inputPath, '/');
+ list (, $user,) = explode('/', $inputPath, 3);
+ $users = array($user);
+ } else if ($input->getOption('all')) {
+ $users = $this->userManager->search('');
+ } else {
+ $users = $input->getArgument('user_id');
+ }
+
+ # no messaging level option means: no full printout but statistics
+ # $quiet means no print at all
+ # $verbose means full printout including statistics
+ # -q -v full stat
+ # 0 0 no yes
+ # 0 1 yes yes
+ # 1 -- no no (quiet overrules verbose)
+ $verbose = $input->getOption('verbose');
+ $quiet = $input->getOption('quiet');
+ # restrict the verbosity level to VERBOSITY_VERBOSE
+ if ($output->getVerbosity()>OutputInterface::VERBOSITY_VERBOSE) {
+ $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
+ }
+ if ($quiet) {
+ $verbose = false;
+ }
+
+ # 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;
+ } else {
+ if ($users_total > 1) {
+ $output->writeln("\nScanning files for $users_total users");
+ }
+ }
+
+ $this->initTools();
+
+ $user_count = 0;
+ foreach ($users as $user) {
+ if (is_object($user)) {
+ $user = $user->getUID();
+ }
+ $path = $inputPath ? $inputPath : '/' . $user;
+ $user_count += 1;
+ if ($this->userManager->userExists($user)) {
+ # add an extra line when verbose is set to optical separate users
+ if ($verbose) {$output->writeln(""); }
+ $output->writeln("Starting scan for user $user_count out of $users_total ($user)");
+ # full: printout data if $verbose was set
+ $this->scanFiles($user, $path, $verbose, $output);
+ } else {
+ $output->writeln("<error>Unknown user $user_count $user</error>");
+ }
+ # check on each user if there was a user interrupt (ctrl-c) and exit foreach
+ if ($this->hasBeenInterrupted()) {
+ break;
+ }
+ }
+
+ # stat: printout statistics if $quiet was not set
+ if (!$quiet) {
+ $this->presentStats($output);
+ }
+ }
+
+ /**
+ * Initialises some useful tools for the Command
+ */
+ protected function initTools() {
+ // 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://secure.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);
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ protected function presentStats(OutputInterface $output) {
+ // Stop the timer
+ $this->execTime += microtime(true);
+ $output->writeln("");
+
+ $headers = [
+ 'Folders', 'Files', 'Elapsed time'
+ ];
+
+ $this->showSummary($headers, null, $output);
+ }
+
+ /**
+ * Shows a summary of operations
+ *
+ * @param string[] $headers
+ * @param string[] $rows
+ * @param OutputInterface $output
+ */
+ protected function showSummary($headers, $rows, OutputInterface $output) {
+ $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
+ *
+ * @return string
+ */
+ protected function formatExecTime() {
+ list($secs, $tens) = explode('.', sprintf("%.1f", ($this->execTime)));
+
+ # if you want to have microseconds add this: . '.' . $tens;
+ return date('H:i:s', $secs);
+ }
+
+}
diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php
new file mode 100644
index 00000000000..1f46efdde0d
--- /dev/null
+++ b/apps/files/lib/Command/TransferOwnership.php
@@ -0,0 +1,240 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @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\Files\Filesystem;
+use OC\Files\View;
+use OCP\Files\FileInfo;
+use OCP\Files\Mount\IMountManager;
+use OCP\IUserManager;
+use OCP\Share\IManager;
+use OCP\Share\IShare;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TransferOwnership extends Command {
+
+ /** @var IUserManager $userManager */
+ private $userManager;
+
+ /** @var IManager */
+ private $shareManager;
+
+ /** @var IMountManager */
+ private $mountManager;
+
+ /** @var FileInfo[] */
+ private $allFiles = [];
+
+ /** @var FileInfo[] */
+ private $encryptedFiles = [];
+
+ /** @var IShare[] */
+ private $shares = [];
+
+ /** @var string */
+ private $sourceUser;
+
+ /** @var string */
+ private $destinationUser;
+
+ /** @var string */
+ private $finalTarget;
+
+ public function __construct(IUserManager $userManager, IManager $shareManager, IMountManager $mountManager) {
+ $this->userManager = $userManager;
+ $this->shareManager = $shareManager;
+ $this->mountManager = $mountManager;
+ parent::__construct();
+ }
+
+ protected function configure() {
+ $this
+ ->setName('files:transfer-ownership')
+ ->setDescription('All files and folders are moved to another user - shares 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'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $this->sourceUser = $input->getArgument('source-user');
+ $this->destinationUser = $input->getArgument('destination-user');
+ if (!$this->userManager->userExists($this->sourceUser)) {
+ $output->writeln("<error>Unknown source user $this->sourceUser</error>");
+ return;
+ }
+ if (!$this->userManager->userExists($this->destinationUser)) {
+ $output->writeln("<error>Unknown destination user $this->destinationUser</error>");
+ return;
+ }
+
+ // target user has to be ready
+ if (!\OC::$server->getEncryptionManager()->isReadyForUser($this->destinationUser)) {
+ $output->writeln("<error>The target user is not ready to accept files. The user has at least to be logged in once.</error>");
+ return;
+ }
+
+ $date = date('c');
+ $this->finalTarget = "$this->destinationUser/files/transferred from $this->sourceUser on $date";
+
+ // setup filesystem
+ Filesystem::initMountPoints($this->sourceUser);
+ Filesystem::initMountPoints($this->destinationUser);
+
+ // analyse source folder
+ $this->analyse($output);
+
+ // collect all the shares
+ $this->collectUsersShares($output);
+
+ // transfer the files
+ $this->transfer($output);
+
+ // restore the shares
+ $this->restoreShares($output);
+ }
+
+ private function walkFiles(View $view, $path, \Closure $callBack) {
+ foreach ($view->getDirectoryContent($path) as $fileInfo) {
+ if (!$callBack($fileInfo)) {
+ return;
+ }
+ if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ $this->walkFiles($view, $fileInfo->getPath(), $callBack);
+ }
+ }
+ }
+
+ /**
+ * @param OutputInterface $output
+ * @throws \Exception
+ */
+ protected function analyse(OutputInterface $output) {
+ $view = new View();
+ $output->writeln("Analysing files of $this->sourceUser ...");
+ $progress = new ProgressBar($output);
+ $progress->start();
+ $self = $this;
+ $this->walkFiles($view, "$this->sourceUser/files",
+ function (FileInfo $fileInfo) use ($progress, $self) {
+ if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ return true;
+ }
+ $progress->advance();
+ $this->allFiles[] = $fileInfo;
+ if ($fileInfo->isEncrypted()) {
+ $this->encryptedFiles[] = $fileInfo;
+ }
+ return true;
+ });
+ $progress->finish();
+ $output->writeln('');
+
+ // no file is allowed to be encrypted
+ if (!empty($this->encryptedFiles)) {
+ $output->writeln("<error>Some files are encrypted - please decrypt them first</error>");
+ foreach($this->encryptedFiles as $encryptedFile) {
+ /** @var FileInfo $encryptedFile */
+ $output->writeln(" " . $encryptedFile->getPath());
+ }
+ throw new \Exception('Execution terminated.');
+ }
+
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ private function collectUsersShares(OutputInterface $output) {
+ $output->writeln("Collecting all share information for files and folder of $this->sourceUser ...");
+
+ $progress = new ProgressBar($output, count($this->shares));
+ foreach([\OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK, \OCP\Share::SHARE_TYPE_REMOTE] as $shareType) {
+ $offset = 0;
+ while (true) {
+ $sharePage = $this->shareManager->getSharesBy($this->sourceUser, $shareType, null, true, 50, $offset);
+ $progress->advance(count($sharePage));
+ if (empty($sharePage)) {
+ break;
+ }
+ $this->shares = array_merge($this->shares, $sharePage);
+ $offset += 50;
+ }
+ }
+
+ $progress->finish();
+ $output->writeln('');
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ protected function transfer(OutputInterface $output) {
+ $view = new View();
+ $output->writeln("Transferring files to $this->finalTarget ...");
+ $view->rename("$this->sourceUser/files", $this->finalTarget);
+ // because the files folder is moved away we need to recreate it
+ $view->mkdir("$this->sourceUser/files");
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ private function restoreShares(OutputInterface $output) {
+ $output->writeln("Restoring shares ...");
+ $progress = new ProgressBar($output, count($this->shares));
+
+ foreach($this->shares as $share) {
+ if ($share->getSharedWith() === $this->destinationUser) {
+ // Unmount the shares before deleting, so we don't try to get the storage later on.
+ $shareMountPoint = $this->mountManager->find('/' . $this->destinationUser . '/files' . $share->getTarget());
+ if ($shareMountPoint) {
+ $this->mountManager->removeMount($shareMountPoint->getMountPoint());
+ }
+ $this->shareManager->deleteShare($share);
+ } else {
+ if ($share->getShareOwner() === $this->sourceUser) {
+ $share->setShareOwner($this->destinationUser);
+ }
+ if ($share->getSharedBy() === $this->sourceUser) {
+ $share->setSharedBy($this->destinationUser);
+ }
+
+ $this->shareManager->updateShare($share);
+ }
+ $progress->advance();
+ }
+ $progress->finish();
+ $output->writeln('');
+ }
+}