Skip to content

Commit c0f51c6

Browse files
committed
Add occ command to repair mtime
Signed-off-by: Louis Chemineau <[email protected]>
1 parent 46e6c49 commit c0f51c6

4 files changed

Lines changed: 288 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: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (c) 2021, Louis Chemineau <[email protected]>
4+
*
5+
* @author Louis Chemineau <[email protected]>
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(string $userId, string $path, OutputInterface $output): void {
91+
$userFolder = $this->rootFolder->getUserFolder($userId);
92+
$user = $this->userManager->get($userId);
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+
case SMB::class:
121+
case FTP::class:
122+
$file->touch();
123+
break;
124+
case AmazonS3::class:
125+
// $file->touch();
126+
break;
127+
default:
128+
$output->writeln(" - Unknown storage $storageClass for $filePath");
129+
break;
130+
}
131+
} catch (ForbiddenException $e) {
132+
$output->writeln("<error>Home storage for user $userId not writable</error>");
133+
$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
134+
} catch (InterruptedException $e) {
135+
# exit the function if ctrl-c has been pressed
136+
$output->writeln('Interrupted by user');
137+
} catch (NotFoundException $e) {
138+
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
139+
} catch (\Exception $e) {
140+
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
141+
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
142+
}
143+
}
144+
145+
$this->connection->commit();
146+
} while (count($invalidFiles) > 0);
147+
}
148+
149+
protected function execute(InputInterface $input, OutputInterface $output): int {
150+
$inputPath = $input->getOption('path');
151+
if ($inputPath) {
152+
$inputPath = '/' . trim($inputPath, '/');
153+
[, $user,] = explode('/', $inputPath, 3);
154+
$users = [$user];
155+
} elseif ($input->getOption('all')) {
156+
$users = $this->userManager->search('');
157+
} else {
158+
$users = $input->getArgument('user_id');
159+
}
160+
161+
# restrict the verbosity level to VERBOSITY_VERBOSE
162+
if ($output->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
163+
$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
164+
}
165+
166+
# check quantity of users to be process and show it on the command line
167+
$users_total = count($users);
168+
if ($users_total === 0) {
169+
$output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
170+
return 1;
171+
}
172+
173+
$this->initTools();
174+
175+
$user_count = 0;
176+
foreach ($users as $user) {
177+
if (is_object($user)) {
178+
$user = $user->getUID();
179+
}
180+
$path = $inputPath ? $inputPath : '/' . $user;
181+
++$user_count;
182+
if ($this->userManager->userExists($user)) {
183+
$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
184+
$this->repairMtimeForUser(
185+
$user,
186+
$path,
187+
$output,
188+
);
189+
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
190+
} else {
191+
$output->writeln("<error>Unknown user $user_count $user</error>");
192+
$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
193+
}
194+
195+
try {
196+
$this->abortIfInterrupted();
197+
} catch (InterruptedException $e) {
198+
break;
199+
}
200+
}
201+
202+
$this->presentStats($output);
203+
return 0;
204+
}
205+
206+
/**
207+
* Initialises some useful tools for the Command
208+
*/
209+
protected function initTools() {
210+
// Start the timer
211+
$this->execTime = -microtime(true);
212+
// Convert PHP errors to exceptions
213+
set_error_handler([$this, 'exceptionErrorHandler'], E_ALL);
214+
}
215+
216+
/**
217+
* Processes PHP errors as exceptions in order to be able to keep track of problems
218+
*
219+
* @see https://www.php.net/manual/en/function.set-error-handler.php
220+
*
221+
* @param int $severity the level of the error raised
222+
* @param string $message
223+
* @param string $file the filename that the error was raised in
224+
* @param int $line the line number the error was raised
225+
*
226+
* @throws \ErrorException
227+
*/
228+
public function exceptionErrorHandler($severity, $message, $file, $line) {
229+
if (!(error_reporting() & $severity)) {
230+
// This error code is not included in error_reporting
231+
return;
232+
}
233+
throw new \ErrorException($message, 0, $severity, $file, $line);
234+
}
235+
236+
/**
237+
* @param OutputInterface $output
238+
*/
239+
protected function presentStats(OutputInterface $output) {
240+
// Stop the timer
241+
$this->execTime += microtime(true);
242+
243+
$headers = [
244+
'Folders', 'Files', 'Elapsed time'
245+
];
246+
247+
$this->showSummary($headers, null, $output);
248+
}
249+
250+
/**
251+
* Shows a summary of operations
252+
*
253+
* @param string[] $headers
254+
* @param string[] $rows
255+
* @param OutputInterface $output
256+
*/
257+
protected function showSummary($headers, $rows, OutputInterface $output) {
258+
$niceDate = $this->formatExecTime();
259+
if (!$rows) {
260+
$rows = [
261+
$this->foldersCounter,
262+
$this->filesCounter,
263+
$niceDate,
264+
];
265+
}
266+
$table = new Table($output);
267+
$table
268+
->setHeaders($headers)
269+
->setRows([$rows]);
270+
$table->render();
271+
}
272+
273+
274+
/**
275+
* Formats microtime into a human readable format
276+
*
277+
* @return string
278+
*/
279+
protected function formatExecTime() {
280+
$secs = round($this->execTime);
281+
# convert seconds into HH:MM:SS form
282+
return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
283+
}
284+
}

0 commit comments

Comments
 (0)