Skip to content

Commit 164c6c2

Browse files
committed
Add occ command to repair mtime
Signed-off-by: Louis Chemineau <louis@chmn.me>
1 parent 46e6c49 commit 164c6c2

4 files changed

Lines changed: 315 additions & 1 deletion

File tree

apps/files/appinfo/info.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<command>OCA\Files\Command\TransferOwnership</command>
3636
<command>OCA\Files\Command\ScanAppData</command>
3737
<command>OCA\Files\Command\RepairTree</command>
38+
<command>OCA\Files\Command\RepairMtime</command>
3839
</commands>
3940

4041
<activity>
@@ -67,4 +68,4 @@
6768
<personal>OCA\Files\Settings\PersonalSettings</personal>
6869
</settings>
6970

70-
</info>
71+
</info>

apps/files/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
'OCA\\Files\\Collaboration\\Resources\\Listener' => $baseDir . '/../lib/Collaboration/Resources/Listener.php',
2828
'OCA\\Files\\Collaboration\\Resources\\ResourceProvider' => $baseDir . '/../lib/Collaboration/Resources/ResourceProvider.php',
2929
'OCA\\Files\\Command\\DeleteOrphanedFiles' => $baseDir . '/../lib/Command/DeleteOrphanedFiles.php',
30+
'OCA\\Files\\Command\\RepairMtime' => $baseDir . '/../lib/Command/RepairMtime.php',
3031
'OCA\\Files\\Command\\RepairTree' => $baseDir . '/../lib/Command/RepairTree.php',
3132
'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php',
3233
'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php',

apps/files/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ComposerStaticInitFiles
4242
'OCA\\Files\\Collaboration\\Resources\\Listener' => __DIR__ . '/..' . '/../lib/Collaboration/Resources/Listener.php',
4343
'OCA\\Files\\Collaboration\\Resources\\ResourceProvider' => __DIR__ . '/..' . '/../lib/Collaboration/Resources/ResourceProvider.php',
4444
'OCA\\Files\\Command\\DeleteOrphanedFiles' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanedFiles.php',
45+
'OCA\\Files\\Command\\RepairMtime' => __DIR__ . '/..' . '/../lib/Command/RepairMtime.php',
4546
'OCA\\Files\\Command\\RepairTree' => __DIR__ . '/..' . '/../lib/Command/RepairTree.php',
4647
'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php',
4748
'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php',
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
4+
*
5+
* @author Louis Chemineau <louis@chmn.me>
6+
*
7+
* @license AGPL-3.0
8+
*
9+
* This code is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License, version 3,
11+
* as published by the Free Software Foundation.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License, version 3,
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>
20+
*
21+
*/
22+
namespace OCA\Files\Command;
23+
24+
use OC\Core\Command\Base;
25+
use OC\Core\Command\InterruptedException;
26+
use OC\DB\Connection;
27+
use OC\ForbiddenException;
28+
use OCP\Files\NotFoundException;
29+
use OCP\Files\IRootFolder;
30+
use OCP\IUserManager;
31+
use Symfony\Component\Console\Helper\Table;
32+
use Symfony\Component\Console\Input\InputArgument;
33+
use Symfony\Component\Console\Input\InputInterface;
34+
use Symfony\Component\Console\Input\InputOption;
35+
use Symfony\Component\Console\Output\OutputInterface;
36+
use OC\Files\Search\SearchComparison;
37+
use OC\Files\Search\SearchOrder;
38+
use OC\Files\Search\SearchQuery;
39+
use OCP\Files\Search\ISearchComparison;
40+
use OCP\Files\Search\ISearchOrder;
41+
use OCA\Files_External\Lib\Storage\FTP;
42+
use OCA\Files_External\Lib\Storage\AmazonS3;
43+
use OCA\Files_External\Lib\Storage\SMB;
44+
use OC\Files\Storage\Local;
45+
use OCP\IDBConnection;
46+
47+
48+
class RepairMtime extends Base {
49+
50+
private IUserManager $userManager;
51+
private IRootFolder $rootFolder;
52+
protected IDBConnection $connection;
53+
54+
protected float $execTime = 0;
55+
protected int $foldersCounter = 0;
56+
protected int $filesCounter = 0;
57+
58+
public function __construct(IDBConnection $connection, IUserManager $userManager, IRootFolder $rootFolder) {
59+
$this->connection = $connection;
60+
$this->userManager = $userManager;
61+
$this->rootFolder = $rootFolder;
62+
parent::__construct();
63+
}
64+
65+
protected function configure() {
66+
parent::configure();
67+
68+
$this
69+
->setName('files:repair-mtime')
70+
->setDescription('Repair files\' mtime')
71+
->addArgument(
72+
'user_id',
73+
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
74+
'will repair mtime for all files of the given user(s)'
75+
)
76+
->addOption(
77+
'path',
78+
'p',
79+
InputArgument::OPTIONAL,
80+
'limit repair 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'
81+
)
82+
->addOption(
83+
'all',
84+
null,
85+
InputOption::VALUE_NONE,
86+
'will repair all files of all known users'
87+
);
88+
}
89+
90+
protected function repairMtimeForUser($user, $path, OutputInterface $output) {
91+
$userFolder = $this->rootFolder->getUserFolder($user);
92+
$user = $this->userManager->get($user);
93+
94+
$fileOffset = 0;
95+
96+
do {
97+
$invalidFiles = $userFolder
98+
// ->get($path)
99+
->search(
100+
new SearchQuery(
101+
new SearchComparison(ISearchComparison::COMPARE_LESS_THAN_EQUAL, 'mtime', 86400),
102+
100,
103+
$fileOffset,
104+
[new SearchOrder(ISearchOrder::DIRECTION_DESCENDING, 'mtime')],
105+
$user
106+
)
107+
);
108+
$fileOffset+=100;
109+
110+
$this->connection->beginTransaction();
111+
112+
foreach ($invalidFiles as $file) {
113+
try {
114+
$filePath = $file->getPath();
115+
$this->filesCounter++;
116+
$storage = $file->getStorage();
117+
$storageClass = get_class($storage);
118+
switch ($storageClass) {
119+
case Local::class:
120+
$output->writeln("Local storage detected for $filePath");
121+
// $file->touch();
122+
break;
123+
case AmazonS3::class:
124+
$output->writeln("S3 storage detected for $filePath");
125+
// $file->touch();
126+
break;
127+
case SMB::class:
128+
$output->writeln("SMB storage detected for $filePath");
129+
// $file->touch();
130+
break;
131+
case FTP::class:
132+
$output->writeln("FTP storage detected for $filePath");
133+
// $file->touch();
134+
break;
135+
default:
136+
$output->writeln(" - Unknown storage $storageClass for $filePath");
137+
break;
138+
}
139+
} catch (ForbiddenException $e) {
140+
$output->writeln("<error>Home storage for user $user not writable</error>");
141+
$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
142+
} catch (InterruptedException $e) {
143+
# exit the function if ctrl-c has been pressed
144+
$output->writeln('Interrupted by user');
145+
} catch (NotFoundException $e) {
146+
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
147+
} catch (\Exception $e) {
148+
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
149+
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
150+
}
151+
}
152+
153+
$this->connection->commit();
154+
} while (count($invalidFiles) > 0);
155+
}
156+
157+
protected function execute(InputInterface $input, OutputInterface $output): int {
158+
$inputPath = $input->getOption('path');
159+
if ($inputPath) {
160+
$inputPath = '/' . trim($inputPath, '/');
161+
[, $user,] = explode('/', $inputPath, 3);
162+
$users = [$user];
163+
} elseif ($input->getOption('all')) {
164+
$users = $this->userManager->search('');
165+
} else {
166+
$users = $input->getArgument('user_id');
167+
}
168+
169+
# restrict the verbosity level to VERBOSITY_VERBOSE
170+
if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
171+
$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
172+
}
173+
174+
# check quantity of users to be process and show it on the command line
175+
$users_total = count($users);
176+
if ($users_total === 0) {
177+
$output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
178+
return 1;
179+
}
180+
181+
$this->initTools();
182+
183+
$user_count = 0;
184+
foreach ($users as $user) {
185+
if (is_object($user)) {
186+
$user = $user->getUID();
187+
}
188+
$path = $inputPath ? $inputPath : '/' . $user;
189+
++$user_count;
190+
if ($this->userManager->userExists($user)) {
191+
$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
192+
$this->repairMtimeForUser(
193+
$user,
194+
$path,
195+
$output,
196+
);
197+
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
198+
} else {
199+
$output->writeln("<error>Unknown user $user_count $user</error>");
200+
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
201+
}
202+
203+
try {
204+
$this->abortIfInterrupted();
205+
} catch (InterruptedException $e) {
206+
break;
207+
}
208+
}
209+
210+
$this->presentStats($output);
211+
return 0;
212+
}
213+
214+
/**
215+
* Initialises some useful tools for the Command
216+
*/
217+
protected function initTools() {
218+
// Start the timer
219+
$this->execTime = -microtime(true);
220+
// Convert PHP errors to exceptions
221+
set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
222+
}
223+
224+
/**
225+
* Processes PHP errors as exceptions in order to be able to keep track of problems
226+
*
227+
* @see https://www.php.net/manual/en/function.set-error-handler.php
228+
*
229+
* @param int $severity the level of the error raised
230+
* @param string $message
231+
* @param string $file the filename that the error was raised in
232+
* @param int $line the line number the error was raised
233+
*
234+
* @throws \ErrorException
235+
*/
236+
public function exceptionErrorHandler($severity, $message, $file, $line) {
237+
if (!(error_reporting() & $severity)) {
238+
// This error code is not included in error_reporting
239+
return;
240+
}
241+
throw new \ErrorException($message, 0, $severity, $file, $line);
242+
}
243+
244+
/**
245+
* @param OutputInterface $output
246+
*/
247+
protected function presentStats(OutputInterface $output) {
248+
// Stop the timer
249+
$this->execTime += microtime(true);
250+
251+
$headers = [
252+
'Folders', 'Files', 'Elapsed time'
253+
];
254+
255+
$this->showSummary($headers, null, $output);
256+
}
257+
258+
/**
259+
* Shows a summary of operations
260+
*
261+
* @param string[] $headers
262+
* @param string[] $rows
263+
* @param OutputInterface $output
264+
*/
265+
protected function showSummary($headers, $rows, OutputInterface $output) {
266+
$niceDate = $this->formatExecTime();
267+
if (!$rows) {
268+
$rows = [
269+
$this->foldersCounter,
270+
$this->filesCounter,
271+
$niceDate,
272+
];
273+
}
274+
$table = new Table($output);
275+
$table
276+
->setHeaders($headers)
277+
->setRows([$rows]);
278+
$table->render();
279+
}
280+
281+
282+
/**
283+
* Formats microtime into a human readable format
284+
*
285+
* @return string
286+
*/
287+
protected function formatExecTime() {
288+
$secs = round($this->execTime);
289+
# convert seconds into HH:MM:SS form
290+
return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
291+
}
292+
293+
protected function reconnectToDatabase(OutputInterface $output): Connection {
294+
/** @var Connection $connection */
295+
$connection = \OC::$server->get(Connection::class);
296+
try {
297+
$connection->close();
298+
} catch (\Exception $ex) {
299+
$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
300+
}
301+
while (!$connection->isConnected()) {
302+
try {
303+
$connection->connect();
304+
} catch (\Exception $ex) {
305+
$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
306+
sleep(60);
307+
}
308+
}
309+
return $connection;
310+
}
311+
}

0 commit comments

Comments
 (0)