2013-09-01 10:40:50 -04:00
< ? php
2024-05-28 10:42:42 -04:00
2013-09-19 13:12:16 -04:00
/**
2024-05-28 10:42:42 -04:00
* SPDX - FileCopyrightText : 2017 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2013-09-19 13:12:16 -04:00
*/
2013-09-01 10:40:50 -04:00
namespace OCA\Files\Command ;
2016-01-29 09:59:57 -05:00
use OC\Core\Command\Base ;
2017-02-10 09:24:25 -05:00
use OC\Core\Command\InterruptedException ;
2021-01-11 10:38:31 -05:00
use OC\DB\Connection ;
use OC\DB\ConnectionAdapter ;
2024-10-10 06:40:31 -04:00
use OC\Files\Utils\Scanner ;
2023-11-06 20:21:29 -05:00
use OC\FilesMetadata\FilesMetadataManager ;
use OC\ForbiddenException ;
use OCP\EventDispatcher\IEventDispatcher ;
2023-06-16 11:56:34 -04:00
use OCP\Files\Events\FileCacheUpdated ;
use OCP\Files\Events\NodeAddedToCache ;
use OCP\Files\Events\NodeRemovedFromCache ;
2022-05-09 08:19:17 -04:00
use OCP\Files\IRootFolder ;
2018-06-28 10:09:25 -04:00
use OCP\Files\Mount\IMountPoint ;
2017-04-19 08:36:38 -04:00
use OCP\Files\NotFoundException ;
2015-11-27 08:02:50 -05:00
use OCP\Files\StorageNotAvailableException ;
2023-11-06 20:21:29 -05:00
use OCP\FilesMetadata\IFilesMetadataManager ;
2016-01-29 09:59:57 -05:00
use OCP\IUserManager ;
2025-02-03 09:34:01 -05:00
use OCP\Server ;
2022-03-17 12:26:27 -04:00
use Psr\Log\LoggerInterface ;
2019-11-22 14:52:10 -05:00
use Symfony\Component\Console\Helper\Table ;
2013-09-01 10:40:50 -04:00
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
2016-01-29 09:59:57 -05:00
class Scan extends Base {
2022-05-09 08:19:17 -04:00
protected float $execTime = 0 ;
protected int $foldersCounter = 0 ;
protected int $filesCounter = 0 ;
2023-05-04 11:06:46 -04:00
protected int $errorsCounter = 0 ;
2023-06-16 11:56:34 -04:00
protected int $newCounter = 0 ;
protected int $updatedCounter = 0 ;
protected int $removedCounter = 0 ;
2013-09-02 12:18:12 -04:00
2022-05-09 08:19:17 -04:00
public function __construct (
2023-07-04 14:43:32 -04:00
private IUserManager $userManager ,
private IRootFolder $rootFolder ,
2023-11-06 20:21:29 -05:00
private FilesMetadataManager $filesMetadataManager ,
2023-08-01 04:32:12 -04:00
private IEventDispatcher $eventDispatcher ,
private LoggerInterface $logger ,
2022-05-09 08:19:17 -04:00
) {
2013-09-02 12:18:12 -04:00
parent :: __construct ();
}
2023-07-04 14:43:32 -04:00
protected function configure () : void {
2016-01-29 09:59:57 -05:00
parent :: configure ();
2013-09-01 10:40:50 -04:00
$this
-> setName ( 'files:scan' )
-> setDescription ( 'rescan filesystem' )
-> addArgument (
2014-06-25 09:22:49 -04:00
'user_id' ,
InputArgument :: OPTIONAL | InputArgument :: IS_ARRAY ,
'will rescan all files of the given user(s)'
)
2014-10-31 12:33:33 -04:00
-> addOption (
'path' ,
2014-10-31 12:39:05 -04:00
'p' ,
2023-12-01 09:55:01 -05:00
InputOption :: VALUE_REQUIRED ,
2014-12-10 05:04:17 -05:00
'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'
2014-10-31 12:33:33 -04:00
)
2022-05-09 08:19:17 -04:00
-> addOption (
'generate-metadata' ,
null ,
2023-11-20 09:32:36 -05:00
InputOption :: VALUE_OPTIONAL ,
'Generate metadata for all scanned files; if specified only generate for named value' ,
''
2022-05-09 08:19:17 -04:00
)
2013-09-01 10:40:50 -04:00
-> addOption (
2014-06-25 09:22:49 -04:00
'all' ,
null ,
InputOption :: VALUE_NONE ,
'will rescan all files of all known users'
2016-05-18 09:29:37 -04:00
) -> addOption (
'unscanned' ,
null ,
InputOption :: VALUE_NONE ,
'only scan files which are marked as not fully scanned'
2018-05-18 17:50:32 -04:00
) -> addOption (
'shallow' ,
null ,
InputOption :: VALUE_NONE ,
'do not scan folders recursively'
2018-06-28 10:09:25 -04:00
) -> addOption (
'home-only' ,
null ,
InputOption :: VALUE_NONE ,
'only scan the home storage, ignoring any mounted external storage or share'
2014-06-25 09:22:49 -04:00
);
2013-09-01 10:40:50 -04:00
}
2023-11-20 09:32:36 -05:00
protected function scanFiles ( string $user , string $path , ? string $scanMetadata , OutputInterface $output , bool $backgroundScan = false , bool $recursive = true , bool $homeOnly = false ) : void {
2016-08-18 08:18:02 -04:00
$connection = $this -> reconnectToDatabase ( $output );
2024-10-10 06:40:31 -04:00
$scanner = new Scanner (
2021-01-11 10:38:31 -05:00
$user ,
new ConnectionAdapter ( $connection ),
2025-02-03 09:34:01 -05:00
Server :: get ( IEventDispatcher :: class ),
Server :: get ( LoggerInterface :: class )
2021-01-11 10:38:31 -05:00
);
2018-10-14 12:38:23 -04:00
2016-01-29 09:59:57 -05:00
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
2024-09-20 11:38:36 -04:00
$scanner -> listen ( '\OC\Files\Utils\Scanner' , 'scanFile' , function ( string $path ) use ( $output , $scanMetadata ) : void {
2018-10-11 14:51:35 -04:00
$output -> writeln ( " \t File \t <info> $path </info> " , OutputInterface :: VERBOSITY_VERBOSE );
++ $this -> filesCounter ;
$this -> abortIfInterrupted ();
2023-11-20 09:32:36 -05:00
if ( $scanMetadata !== null ) {
2023-07-04 14:43:32 -04:00
$node = $this -> rootFolder -> get ( $path );
2023-11-06 20:21:29 -05:00
$this -> filesMetadataManager -> refreshMetadata (
$node ,
2023-11-20 09:32:36 -05:00
( $scanMetadata !== '' ) ? IFilesMetadataManager :: PROCESS_NAMED : IFilesMetadataManager :: PROCESS_LIVE | IFilesMetadataManager :: PROCESS_BACKGROUND ,
$scanMetadata
2023-11-06 20:21:29 -05:00
);
2022-05-09 08:19:17 -04:00
}
2018-10-11 14:51:35 -04:00
});
2024-09-20 11:38:36 -04:00
$scanner -> listen ( '\OC\Files\Utils\Scanner' , 'scanFolder' , function ( $path ) use ( $output ) : void {
2018-10-11 14:51:35 -04:00
$output -> writeln ( " \t Folder \t <info> $path </info> " , OutputInterface :: VERBOSITY_VERBOSE );
++ $this -> foldersCounter ;
$this -> abortIfInterrupted ();
});
2024-09-20 11:38:36 -04:00
$scanner -> listen ( '\OC\Files\Utils\Scanner' , 'StorageNotAvailable' , function ( StorageNotAvailableException $e ) use ( $output ) : void {
2018-10-11 14:51:35 -04:00
$output -> writeln ( 'Error while scanning, storage not available (' . $e -> getMessage () . ')' , OutputInterface :: VERBOSITY_VERBOSE );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2018-10-11 14:51:35 -04:00
});
2024-09-20 11:38:36 -04:00
$scanner -> listen ( '\OC\Files\Utils\Scanner' , 'normalizedNameMismatch' , function ( $fullPath ) use ( $output ) : void {
2021-11-10 09:09:25 -05:00
$output -> writeln ( " \t <error>Entry \" " . $fullPath . '" will not be accessible due to incompatible encoding</error>' );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2016-04-29 04:43:07 -04:00
});
2015-12-15 12:45:49 -05:00
2024-09-20 11:38:36 -04:00
$this -> eventDispatcher -> addListener ( NodeAddedToCache :: class , function () : void {
2023-06-16 11:56:34 -04:00
++ $this -> newCounter ;
});
2024-09-20 11:38:36 -04:00
$this -> eventDispatcher -> addListener ( FileCacheUpdated :: class , function () : void {
2023-06-16 11:56:34 -04:00
++ $this -> updatedCounter ;
});
2024-09-20 11:38:36 -04:00
$this -> eventDispatcher -> addListener ( NodeRemovedFromCache :: class , function () : void {
2023-06-16 11:56:34 -04:00
++ $this -> removedCounter ;
});
2014-06-25 09:22:49 -04:00
try {
2016-05-18 09:29:37 -04:00
if ( $backgroundScan ) {
$scanner -> backgroundScan ( $path );
2017-04-19 08:36:38 -04:00
} else {
2018-06-28 10:09:25 -04:00
$scanner -> scan ( $path , $recursive , $homeOnly ? [ $this , 'filterHomeMount' ] : null );
2016-05-18 09:29:37 -04:00
}
2014-06-25 09:22:49 -04:00
} catch ( ForbiddenException $e ) {
2022-06-14 07:10:29 -04:00
$output -> writeln ( " <error>Home storage for user $user not writable or 'files' subdirectory missing</error> " );
2021-06-17 07:53:11 -04:00
$output -> writeln ( ' ' . $e -> getMessage ());
2018-10-07 12:17:29 -04:00
$output -> writeln ( 'Make sure you\'re running the scan command only as the user the web server runs as' );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2017-02-10 09:24:25 -05:00
} catch ( InterruptedException $e ) {
# exit the function if ctrl-c has been pressed
$output -> writeln ( 'Interrupted by user' );
2017-04-19 08:36:38 -04:00
} catch ( NotFoundException $e ) {
$output -> writeln ( '<error>Path not found: ' . $e -> getMessage () . '</error>' );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2016-01-26 12:42:03 -05:00
} catch ( \Exception $e ) {
2017-02-10 09:24:25 -05:00
$output -> writeln ( '<error>Exception during scan: ' . $e -> getMessage () . '</error>' );
$output -> writeln ( '<error>' . $e -> getTraceAsString () . '</error>' );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2014-06-25 09:22:49 -04:00
}
2013-09-01 10:40:50 -04:00
}
2023-07-04 14:43:32 -04:00
public function filterHomeMount ( IMountPoint $mountPoint ) : bool {
2018-06-28 10:09:25 -04:00
// any mountpoint inside '/$user/files/'
return substr_count ( $mountPoint -> getMountPoint (), '/' ) <= 3 ;
}
2015-12-15 12:45:49 -05:00
2020-06-26 09:12:11 -04:00
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
2015-07-15 08:08:06 -04:00
$inputPath = $input -> getOption ( 'path' );
if ( $inputPath ) {
$inputPath = '/' . trim ( $inputPath , '/' );
2021-01-12 04:15:48 -05:00
[, $user ,] = explode ( '/' , $inputPath , 3 );
2020-03-26 04:30:18 -04:00
$users = [ $user ];
2020-04-10 04:35:09 -04:00
} elseif ( $input -> getOption ( 'all' )) {
2013-09-02 12:18:12 -04:00
$users = $this -> userManager -> search ( '' );
2013-09-01 10:40:50 -04:00
} else {
$users = $input -> getArgument ( 'user_id' );
}
2014-12-10 05:04:17 -05:00
2016-01-26 12:42:03 -05:00
# check quantity of users to be process and show it on the command line
$users_total = count ( $users );
if ( $users_total === 0 ) {
2018-10-11 14:51:35 -04:00
$output -> writeln ( '<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>' );
2023-07-04 14:43:32 -04:00
return self :: FAILURE ;
2016-01-26 12:42:03 -05:00
}
2023-04-27 04:46:37 -04:00
$this -> initTools ( $output );
2015-12-15 12:45:49 -05:00
2023-11-20 09:32:36 -05:00
// getOption() logic on VALUE_OPTIONAL
$metadata = null ; // null if --generate-metadata is not set, empty if option have no value, value if set
if ( $input -> getOption ( 'generate-metadata' ) !== '' ) {
$metadata = $input -> getOption ( 'generate-metadata' ) ? ? '' ;
}
2016-01-26 12:42:03 -05:00
$user_count = 0 ;
2013-09-01 10:40:50 -04:00
foreach ( $users as $user ) {
2013-09-02 12:18:12 -04:00
if ( is_object ( $user )) {
$user = $user -> getUID ();
}
2023-07-04 14:43:32 -04:00
$path = $inputPath ? : '/' . $user ;
2018-10-11 14:51:35 -04:00
++ $user_count ;
2014-09-24 09:48:54 -04:00
if ( $this -> userManager -> userExists ( $user )) {
2016-01-26 12:42:03 -05:00
$output -> writeln ( " Starting scan for user $user_count out of $users_total ( $user ) " );
2023-11-20 09:32:36 -05:00
$this -> scanFiles ( $user , $path , $metadata , $output , $input -> getOption ( 'unscanned' ), ! $input -> getOption ( 'shallow' ), $input -> getOption ( 'home-only' ));
2018-10-11 14:51:35 -04:00
$output -> writeln ( '' , OutputInterface :: VERBOSITY_VERBOSE );
2014-09-24 09:48:54 -04:00
} else {
2016-01-26 12:42:03 -05:00
$output -> writeln ( " <error>Unknown user $user_count $user </error> " );
2018-10-11 14:51:35 -04:00
$output -> writeln ( '' , OutputInterface :: VERBOSITY_VERBOSE );
2016-01-26 12:42:03 -05:00
}
2018-10-07 12:17:29 -04:00
try {
2018-10-08 07:05:00 -04:00
$this -> abortIfInterrupted ();
2018-10-11 14:51:35 -04:00
} catch ( InterruptedException $e ) {
2016-01-26 12:42:03 -05:00
break ;
2014-09-24 09:48:54 -04:00
}
2013-09-01 10:40:50 -04:00
}
2015-12-15 12:45:49 -05:00
2018-10-11 14:51:35 -04:00
$this -> presentStats ( $output );
2023-07-04 14:43:32 -04:00
return self :: SUCCESS ;
2015-12-15 12:45:49 -05:00
}
/**
* Initialises some useful tools for the Command
*/
2023-07-04 14:43:32 -04:00
protected function initTools ( OutputInterface $output ) : void {
2015-12-15 12:45:49 -05:00
// Start the timer
$this -> execTime = - microtime ( true );
// Convert PHP errors to exceptions
2023-04-27 04:46:37 -04:00
set_error_handler (
fn ( int $severity , string $message , string $file , int $line ) : bool =>
$this -> exceptionErrorHandler ( $output , $severity , $message , $file , $line ),
E_ALL
);
2015-12-15 12:45:49 -05:00
}
/**
2023-04-27 04:46:37 -04:00
* Processes PHP errors in order to be able to show them in the output
2015-12-15 12:45:49 -05:00
*
2020-09-17 11:23:07 -04:00
* @ see https :// www . php . net / manual / en / function . set - error - handler . php
2015-12-15 12:45:49 -05:00
*
* @ 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
*/
2023-04-27 04:46:37 -04:00
public function exceptionErrorHandler ( OutputInterface $output , int $severity , string $message , string $file , int $line ) : bool {
if (( $severity === E_DEPRECATED ) || ( $severity === E_USER_DEPRECATED )) {
// Do not show deprecation warnings
return false ;
2015-12-15 12:45:49 -05:00
}
2023-04-27 04:46:37 -04:00
$e = new \ErrorException ( $message , 0 , $severity , $file , $line );
2023-05-02 04:01:38 -04:00
$output -> writeln ( '<error>Error during scan: ' . $e -> getMessage () . '</error>' );
$output -> writeln ( '<error>' . $e -> getTraceAsString () . '</error>' , OutputInterface :: VERBOSITY_VERY_VERBOSE );
2023-05-04 11:06:46 -04:00
++ $this -> errorsCounter ;
2023-04-27 04:46:37 -04:00
return true ;
2013-09-01 10:40:50 -04:00
}
2015-12-15 12:45:49 -05:00
2023-07-04 14:43:32 -04:00
protected function presentStats ( OutputInterface $output ) : void {
2015-12-15 12:45:49 -05:00
// Stop the timer
$this -> execTime += microtime ( true );
2023-06-16 11:56:34 -04:00
$this -> logger -> info ( " Completed scan of { $this -> filesCounter } files in { $this -> foldersCounter } folder. Found { $this -> newCounter } new, { $this -> updatedCounter } updated and { $this -> removedCounter } removed items " );
2015-12-15 12:45:49 -05:00
$headers = [
2023-05-04 11:06:46 -04:00
'Folders' ,
'Files' ,
2023-06-16 11:56:34 -04:00
'New' ,
'Updated' ,
'Removed' ,
2023-05-04 11:06:46 -04:00
'Errors' ,
'Elapsed time' ,
2015-12-15 12:45:49 -05:00
];
$niceDate = $this -> formatExecTime ();
2023-05-04 11:06:46 -04:00
$rows = [
$this -> foldersCounter ,
$this -> filesCounter ,
2023-06-16 11:56:34 -04:00
$this -> newCounter ,
$this -> updatedCounter ,
$this -> removedCounter ,
2023-05-04 11:06:46 -04:00
$this -> errorsCounter ,
$niceDate ,
];
2015-12-15 12:45:49 -05:00
$table = new Table ( $output );
$table
-> setHeaders ( $headers )
-> setRows ([ $rows ]);
$table -> render ();
}
/**
2023-07-04 14:43:32 -04:00
* Formats microtime into a human - readable format
2015-12-15 12:45:49 -05:00
*/
2023-07-04 14:43:32 -04:00
protected function formatExecTime () : string {
2022-06-12 09:01:22 -04:00
$secs = ( int ) round ( $this -> execTime );
2019-02-22 09:29:03 -05:00
# convert seconds into HH:MM:SS form
2022-05-09 08:19:17 -04:00
return sprintf ( '%02d:%02d:%02d' , ( int )( $secs / 3600 ), (( int )( $secs / 60 ) % 60 ), $secs % 60 );
2015-12-15 12:45:49 -05:00
}
2021-01-11 10:38:31 -05:00
protected function reconnectToDatabase ( OutputInterface $output ) : Connection {
/** @var Connection $connection */
2025-02-03 09:34:01 -05:00
$connection = Server :: get ( Connection :: class );
2016-08-18 08:18:02 -04:00
try {
$connection -> close ();
} catch ( \Exception $ex ) {
$output -> writeln ( " <info>Error while disconnecting from database: { $ex -> getMessage () } </info> " );
}
while ( ! $connection -> isConnected ()) {
try {
$connection -> connect ();
} catch ( \Exception $ex ) {
$output -> writeln ( " <info>Error while re-connecting to database: { $ex -> getMessage () } </info> " );
sleep ( 60 );
}
}
return $connection ;
}
2013-09-01 10:40:50 -04:00
}