1<?php 2/** 3 * @author Ilja Neumann <ineumann@owncloud.com> 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 22namespace OCA\Files\Command; 23 24use OC\Files\FileInfo; 25use OC\Files\Storage\FailedStorage; 26use OC\Files\Storage\Wrapper\Checksum; 27use OCA\Files_Sharing\ISharedStorage; 28use OCP\Files\IRootFolder; 29use OCP\Files\Node; 30use OCP\Files\NotFoundException; 31use OCP\Files\Storage\IStorage; 32use OCP\Files\StorageNotAvailableException; 33use OCP\IUser; 34use OCP\IUserManager; 35use Symfony\Component\Console\Command\Command; 36use Symfony\Component\Console\Input\InputInterface; 37use Symfony\Component\Console\Input\InputOption; 38use Symfony\Component\Console\Output\OutputInterface; 39 40/** 41 * Recomputes checksums for all files and compares them to filecache 42 * entries. Provides repair option on mismatch. 43 * 44 * @package OCA\Files\Command 45 */ 46class VerifyChecksums extends Command { 47 public const EXIT_NO_ERRORS = 0; 48 public const EXIT_CHECKSUM_ERRORS = 1; 49 public const EXIT_INVALID_ARGS = 2; 50 51 /** 52 * @var IRootFolder 53 */ 54 private $rootFolder; 55 /** 56 * @var IUserManager 57 */ 58 private $userManager; 59 60 private $exitStatus = self::EXIT_NO_ERRORS; 61 62 /** 63 * VerifyChecksums constructor. 64 * 65 * @param IRootFolder $rootFolder 66 * @param IUserManager $userManager 67 */ 68 public function __construct(IRootFolder $rootFolder, IUserManager $userManager) { 69 parent::__construct(null); 70 $this->rootFolder = $rootFolder; 71 $this->userManager = $userManager; 72 } 73 74 protected function configure() { 75 $this 76 ->setName('files:checksums:verify') 77 ->setDescription('Get all checksums in filecache and compares them by recalculating the checksum of the file.') 78 ->addOption('repair', 'r', InputOption::VALUE_NONE, 'Repair filecache-entry with mismatched checksums.') 79 ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Specific user to check') 80 ->addOption('path', 'p', InputOption::VALUE_REQUIRED, 'Path to check relative to user folder, i.e, relative to /john/files. e.g tree/apple', ''); 81 } 82 83 /** 84 * @param InputInterface $input 85 * @param OutputInterface $output 86 * @return int 87 * @throws NotFoundException 88 * @throws \OCP\Files\InvalidPathException 89 * @throws \OCP\Files\StorageNotAvailableException 90 */ 91 public function execute(InputInterface $input, OutputInterface $output) { 92 $pathOption = $input->getOption('path'); 93 $userName = $input->getOption('user'); 94 95 $scanUserFunction = function (IUser $user) use ($input, $output) { 96 $output->writeln('<info>Starting to verify checksums for '.$user->getUID().'</info>'); 97 $userFolder = $this->rootFolder->getUserFolder($user->getUID())->getParent(); 98 $this->verifyChecksumsForFolder($userFolder, $input, $output); 99 }; 100 101 if ($userName) { 102 if (!$this->userManager->userExists($userName)) { 103 $output->writeln("<error>User \"$userName\" does not exist</error>"); 104 $this->exitStatus = self::EXIT_INVALID_ARGS; 105 return $this->exitStatus; 106 } 107 if (!$pathOption) { 108 $scanUserFunction($this->userManager->get($userName)); 109 } else { 110 try { 111 $userFolder = $this->rootFolder->getUserFolder($userName); 112 $node = $userFolder->get($pathOption); 113 } catch (NotFoundException $ex) { 114 $output->writeln("<error>Path \"{$ex->getMessage()}\" not found.</error>"); 115 $this->exitStatus = self::EXIT_INVALID_ARGS; 116 return $this->exitStatus; 117 } 118 if ($node === FileInfo::TYPE_FILE) { 119 $this->verifyChecksumsForFile($node, $input, $output); 120 } else { 121 $this->verifyChecksumsForFolder($node, $input, $output); 122 } 123 } 124 } else { 125 if ($pathOption) { 126 $output->writeln("<error>Please provide user when path is provided as argument</error>"); 127 $this->exitStatus = self::EXIT_INVALID_ARGS; 128 return $this->exitStatus; 129 } 130 $output->writeln('<info>This operation might take quite some time.</info>'); 131 $this->userManager->callForAllUsers($scanUserFunction); 132 $output->writeln('<info>Operation successfully completed</info>'); 133 } 134 135 return $this->exitStatus; 136 } 137 138 /** 139 * Verifies checksum of a file 140 * 141 * @param Node $file 142 * @param InputInterface $input 143 * @param OutputInterface $output 144 * @throws NotFoundException 145 * @throws \OCP\Files\InvalidPathException 146 * @throws \OCP\Files\StorageNotAvailableException 147 */ 148 private function verifyChecksumsForFile($file, InputInterface $input, OutputInterface $output) { 149 $path = $file->getInternalPath(); 150 $currentChecksums = $file->getChecksum(); 151 $storage = $file->getStorage(); 152 $storageId = $storage->getId(); 153 154 if ($storage->instanceOfStorage(ISharedStorage::class) || $storage->instanceOfStorage(FailedStorage::class)) { 155 return; 156 } 157 158 try { 159 $fileExistsOnDisk = self::fileExistsOnDisk($file); 160 } catch (StorageNotAvailableException $e) { 161 $output->writeln("Skipping $storageId/$path => Storage is not available", OutputInterface::VERBOSITY_VERBOSE); 162 return; 163 } 164 165 if (!$fileExistsOnDisk) { 166 $output->writeln("Skipping $storageId/$path => File is in file-cache but doesn't exist on storage/disk", OutputInterface::VERBOSITY_VERBOSE); 167 return; 168 } 169 170 if (!$file->isReadable()) { 171 $output->writeln("Skipping $storageId/$path => File not readable", OutputInterface::VERBOSITY_VERBOSE); 172 return; 173 } 174 175 // Files without calculated checksum can't cause checksum errors 176 if (empty($currentChecksums)) { 177 $output->writeln("Skipping $storageId/$path => No Checksum", OutputInterface::VERBOSITY_VERBOSE); 178 return; 179 } 180 181 $output->writeln("Checking $storageId/$path => $currentChecksums", OutputInterface::VERBOSITY_VERBOSE); 182 $actualChecksums = self::calculateActualChecksums($path, $file->getStorage()); 183 if ($actualChecksums !== $currentChecksums) { 184 $output->writeln( 185 "<info>Mismatch for $storageId/$path:\n Filecache:\t$currentChecksums\n Actual:\t$actualChecksums</info>" 186 ); 187 188 $this->exitStatus = self::EXIT_CHECKSUM_ERRORS; 189 190 if ($input->getOption('repair')) { 191 $output->writeln("<info>Repairing $path</info>"); 192 $this->updateChecksumsForNode($file, $actualChecksums); 193 $this->exitStatus = self::EXIT_NO_ERRORS; 194 } 195 } 196 } 197 198 /** 199 * Verifies checksums of a folder and its children 200 * 201 * @param Node $folder 202 * @param InputInterface $input 203 * @param OutputInterface $output 204 * @throws NotFoundException 205 * @throws \OCP\Files\InvalidPathException 206 * @throws \OCP\Files\StorageNotAvailableException 207 */ 208 private function verifyChecksumsForFolder($folder, InputInterface $input, OutputInterface $output) { 209 $folderQueue = [$folder]; 210 while ($currentFolder = \array_pop($folderQueue)) { 211 '@phan-var \OCP\Files\Folder $currentFolder'; 212 $currentFolderPath = $currentFolder->getPath(); 213 try { 214 $nodes = $currentFolder->getDirectoryListing(); 215 } catch (NotFoundException $e) { 216 $nodes = []; 217 $output->writeln("Skipping $currentFolderPath => Directory could not be found"); 218 } catch (StorageNotAvailableException $e) { 219 $nodes = []; 220 $output->writeln("Skipping $currentFolderPath => Storage is not available"); 221 } catch (\Exception $e) { 222 $nodes = []; 223 $output->writeln("Skipping $currentFolderPath => " . $e->getMessage()); 224 } 225 foreach ($nodes as $node) { 226 if ($node->getType() === FileInfo::TYPE_FOLDER) { 227 $folderQueue[] = $node; 228 } else { 229 $this->verifyChecksumsForFile($node, $input, $output); 230 } 231 } 232 /* Force garbage collector to clear memory */ 233 unset($nodes); 234 } 235 } 236 237 /** 238 * @param Node $node 239 * @param $correctChecksum 240 * @throws NotFoundException 241 * @throws \OCP\Files\InvalidPathException 242 * @throws \OCP\Files\StorageNotAvailableException 243 */ 244 private function updateChecksumsForNode(Node $node, $correctChecksum) { 245 $storage = $node->getStorage(); 246 $cache = $storage->getCache(); 247 $cache->update( 248 $node->getId(), 249 ['checksum' => $correctChecksum] 250 ); 251 } 252 253 /** 254 * 255 * @param Node $node 256 * @return bool 257 */ 258 private static function fileExistsOnDisk(Node $node) { 259 $statResult = @$node->stat(); 260 return \is_array($statResult) && isset($statResult['size']) && $statResult['size'] !== false; 261 } 262 263 /** 264 * @param $path 265 * @param IStorage $storage 266 * @return string 267 * @throws \OCP\Files\StorageNotAvailableException 268 */ 269 private static function calculateActualChecksums($path, IStorage $storage) { 270 return \sprintf( 271 Checksum::CHECKSUMS_DB_FORMAT, 272 $storage->hash('sha1', $path), 273 $storage->hash('md5', $path), 274 $storage->hash('adler32', $path) 275 ); 276 } 277} 278