Completed
Push — master ( e53ccb...9be90b )
by Thomas
54:38 queued 41:24
created

VerifyChecksums::execute()   D

Complexity

Conditions 18
Paths 20

Size

Total Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
nc 20
nop 2
dl 0
loc 82
rs 4.8666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Ilja Neumann <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2018, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OCA\Files\Command;
23
24
use OC\Files\FileInfo;
25
use OC\Files\Storage\FailedStorage;
26
use OC\Files\Storage\Wrapper\Checksum;
27
use OCA\Files_Sharing\ISharedStorage;
28
use OCP\Files\IRootFolder;
29
use OCP\Files\Node;
30
use OCP\Files\NotFoundException;
31
use OCP\Files\Storage\IStorage;
32
use OCP\IUser;
33
use OCP\IUserManager;
34
use Symfony\Component\Console\Command\Command;
35
use Symfony\Component\Console\Input\InputInterface;
36
use Symfony\Component\Console\Input\InputOption;
37
use Symfony\Component\Console\Output\OutputInterface;
38
39
/**
40
 * Recomputes checksums for all files and compares them to filecache
41
 * entries. Provides repair option on mismatch.
42
 *
43
 * @package OCA\Files\Command
44
 */
45
class VerifyChecksums extends Command {
46
	const EXIT_NO_ERRORS = 0;
47
	const EXIT_CHECKSUM_ERRORS = 1;
48
	const EXIT_INVALID_ARGS = 2;
49
50
	/**
51
	 * @var IRootFolder
52
	 */
53
	private $rootFolder;
54
	/**
55
	 * @var IUserManager
56
	 */
57
	private $userManager;
58
59
	private $exitStatus = self::EXIT_NO_ERRORS;
60
61
	/**
62
	 * VerifyChecksums constructor.
63
	 *
64
	 * @param IRootFolder $rootFolder
65
	 * @param IUserManager $userManager
66
	 */
67
	public function __construct(IRootFolder $rootFolder, IUserManager $userManager) {
68
		parent::__construct(null);
69
		$this->rootFolder = $rootFolder;
70
		$this->userManager = $userManager;
71
	}
72
73
	protected function configure() {
74
		$this
75
			->setName('files:checksums:verify')
76
			->setDescription('Get all checksums in filecache and compares them by recalculating the checksum of the file.')
77
			->addOption('repair', 'r', InputOption::VALUE_NONE, 'Repair filecache-entry with mismatched checksums.')
78
			->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Specific user to check')
79
			->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Path to check relative to data e.g /john/files/', '');
80
	}
81
82
	public function execute(InputInterface $input, OutputInterface $output) {
83
		$pathOption = $input->getOption('path');
84
		$userName = $input->getOption('user');
85
86
		if (!$pathOption && !$userName) {
87
			$output->writeln('<info>This operation might take quite some time.</info>');
88
		}
89
90
		if ($pathOption && $userName) {
91
			$output->writeln('<error>Please use either path or user exclusively</error>');
92
			$this->exitStatus = self::EXIT_INVALID_ARGS;
93
		}
94
95
		$walkFunction = function (Node $node) use ($input, $output) {
96
			$path = $node->getInternalPath();
97
			$currentChecksums = $node->getChecksum();
98
			$owner = $node->getOwner()->getUID();
99
			$storage = $node->getStorage();
100
101
			if ($storage->instanceOfStorage(ISharedStorage::class) || $storage->instanceOfStorage(FailedStorage::class)) {
102
				return;
103
			}
104
105
			if (!self::fileExistsOnDisk($node)) {
106
				$output->writeln("Skipping $owner/$path => File is in file-cache but doesn't exist on storage/disk", OutputInterface::VERBOSITY_VERBOSE);
107
				return;
108
			}
109
110
			if (!$node->isReadable()) {
111
				$output->writeln("Skipping $owner/$path => File not readable", OutputInterface::VERBOSITY_VERBOSE);
112
				return;
113
			}
114
115
			// Files without calculated checksum can't cause checksum errors
116
			if (empty($currentChecksums)) {
117
				$output->writeln("Skipping $owner/$path => No Checksum", OutputInterface::VERBOSITY_VERBOSE);
118
				return;
119
			}
120
121
			$output->writeln("Checking $owner/$path => $currentChecksums", OutputInterface::VERBOSITY_VERBOSE);
122
			$actualChecksums = self::calculateActualChecksums($path, $node->getStorage());
123
			if ($actualChecksums !== $currentChecksums) {
124
				$output->writeln(
125
					"<info>Mismatch for $owner/$path:\n Filecache:\t$currentChecksums\n Actual:\t$actualChecksums</info>"
126
				);
127
128
				$this->exitStatus = self::EXIT_CHECKSUM_ERRORS;
129
130
				if ($input->getOption('repair')) {
131
					$output->writeln("<info>Repairing $path</info>");
132
					$this->updateChecksumsForNode($node, $actualChecksums);
133
					$this->exitStatus = self::EXIT_NO_ERRORS;
134
				}
135
			}
136
		};
137
138
		$scanUserFunction = function (IUser $user) use ($input, $output, $walkFunction) {
139
			$userFolder = $this->rootFolder->getUserFolder($user->getUID())->getParent();
140
			$this->walkNodes($userFolder->getDirectoryListing(), $walkFunction);
141
		};
142
143
		if ($userName && $this->userManager->userExists($userName)) {
144
			$scanUserFunction($this->userManager->get($userName));
145
		} elseif ($userName && !$this->userManager->userExists($userName)) {
146
			$output->writeln("<error>User \"$userName\" does not exist</error>");
147
			$this->exitStatus = self::EXIT_INVALID_ARGS;
148
		} elseif ($input->getOption('path')) {
149
			try {
150
				$node = $this->rootFolder->get($input->getOption('path'));
151
			} catch (NotFoundException $ex) {
152
				$output->writeln("<error>Path \"{$ex->getMessage()}\" not found.</error>");
153
				$this->exitStatus = self::EXIT_INVALID_ARGS;
154
				return $this->exitStatus;
155
			}
156
157
			$this->walkNodes([$node], $walkFunction);
158
		} else {
159
			$this->userManager->callForAllUsers($scanUserFunction);
160
		}
161
162
		return $this->exitStatus;
163
	}
164
165
	/**
166
	 * Recursive walk nodes
167
	 *
168
	 * @param Node[] $nodes
169
	 * @param \Closure $callBack
170
	 */
171
	private function walkNodes(array $nodes, \Closure $callBack) {
172
		foreach ($nodes as $node) {
173
			if ($node->getType() === FileInfo::TYPE_FOLDER) {
174
				$this->walkNodes($node->getDirectoryListing(), $callBack);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface OCP\Files\Node as the method getDirectoryListing() does only exist in the following implementations of said interface: OC\Files\Meta\MetaFileIdNode, OC\Files\Meta\MetaRootNode, OC\Files\Meta\MetaVersionCollection, OC\Files\Node\AbstractFolder, OC\Files\Node\Folder, OC\Files\Node\LazyRoot, OC\Files\Node\NonExistingFolder, OC\Files\Node\Root.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
175
			} else {
176
				$callBack($node);
177
			}
178
		}
179
	}
180
181
	/**
182
	 * @param Node $node
183
	 * @param $correctChecksum
184
	 * @throws NotFoundException
185
	 * @throws \OCP\Files\InvalidPathException
186
	 * @throws \OCP\Files\StorageNotAvailableException
187
	 */
188
	private function updateChecksumsForNode(Node $node, $correctChecksum) {
189
		$storage = $node->getStorage();
190
		$cache = $storage->getCache();
191
		$cache->update(
192
			$node->getId(),
193
			['checksum' => $correctChecksum]
194
		);
195
	}
196
197
	/**
198
	 *
199
	 * @param Node $node
200
	 * @return bool
201
	 */
202
	private static function fileExistsOnDisk(Node $node) {
203
		$statResult = @$node->stat();
204
		return \is_array($statResult) && isset($statResult['size']) && $statResult['size'] !== false;
205
	}
206
207
	/**
208
	 * @param $path
209
	 * @param IStorage $storage
210
	 * @return string
211
	 * @throws \OCP\Files\StorageNotAvailableException
212
	 */
213
	private static function calculateActualChecksums($path, IStorage $storage) {
214
		return \sprintf(
215
			Checksum::CHECKSUMS_DB_FORMAT,
216
			$storage->hash('sha1', $path),
217
			$storage->hash('md5', $path),
218
			$storage->hash('adler32', $path)
219
		);
220
	}
221
}
222