2020-08-06 15:30:51 -04:00
< ? php
declare ( strict_types = 1 );
2020-08-24 08:54:25 -04:00
2020-08-06 15:30:51 -04:00
/**
2024-05-24 13:43:47 -04:00
* SPDX - FileCopyrightText : 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2020-08-06 15:30:51 -04:00
*/
namespace OC\Core\Command\Preview ;
use bantu\IniGetWrapper\IniGetWrapper ;
use OC\Preview\Storage\Root ;
use OCP\Files\Folder ;
use OCP\Files\IRootFolder ;
use OCP\Files\NotFoundException ;
use OCP\IConfig ;
2020-09-17 10:03:34 -04:00
use OCP\Lock\ILockingProvider ;
use OCP\Lock\LockedException ;
2022-04-12 11:55:01 -04:00
use Psr\Log\LoggerInterface ;
2020-08-06 15:30:51 -04:00
use Symfony\Component\Console\Command\Command ;
use Symfony\Component\Console\Helper\ProgressBar ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
use Symfony\Component\Console\Question\ConfirmationQuestion ;
2022-12-19 09:06:48 -05:00
use function pcntl_signal ;
2020-08-06 15:30:51 -04:00
class Repair extends Command {
2022-04-12 11:55:01 -04:00
private bool $stopSignalReceived = false ;
private int $memoryLimit ;
private int $memoryTreshold ;
2020-08-06 15:30:51 -04:00
2023-06-12 11:10:19 -04:00
public function __construct (
protected IConfig $config ,
private IRootFolder $rootFolder ,
private LoggerInterface $logger ,
IniGetWrapper $phpIni ,
private ILockingProvider $lockingProvider ,
) {
2022-04-12 11:55:01 -04:00
$this -> memoryLimit = ( int ) $phpIni -> getBytes ( 'memory_limit' );
2020-08-06 15:30:51 -04:00
$this -> memoryTreshold = $this -> memoryLimit - 25 * 1024 * 1024 ;
parent :: __construct ();
}
protected function configure () {
$this
-> setName ( 'preview:repair' )
-> setDescription ( 'distributes the existing previews into subfolders' )
-> addOption ( 'batch' , 'b' , InputOption :: VALUE_NONE , 'Batch mode - will not ask to start the migration and start it right away.' )
2021-12-09 10:52:15 -05:00
-> addOption ( 'dry' , 'd' , InputOption :: VALUE_NONE , 'Dry mode - will not create, move or delete any files - in combination with the verbose mode one could check the operations.' )
-> addOption ( 'delete' , null , InputOption :: VALUE_NONE , 'Delete instead of migrating them. Usefull if too many entries to migrate.' );
2020-08-06 15:30:51 -04:00
}
protected function execute ( InputInterface $input , OutputInterface $output ) : int {
if ( $this -> memoryLimit !== - 1 ) {
2020-10-05 09:12:57 -04:00
$limitInMiB = round ( $this -> memoryLimit / 1024 / 1024 , 1 );
$thresholdInMiB = round ( $this -> memoryTreshold / 1024 / 1024 , 1 );
2020-08-06 15:30:51 -04:00
$output -> writeln ( " Memory limit is $limitInMiB MiB " );
$output -> writeln ( " Memory threshold is $thresholdInMiB MiB " );
2024-08-23 09:10:27 -04:00
$output -> writeln ( '' );
2020-08-06 15:30:51 -04:00
$memoryCheckEnabled = true ;
} else {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'No memory limit in place - disabled memory check. Set a PHP memory limit to automatically stop the execution of this migration script once memory consumption is close to this limit.' );
$output -> writeln ( '' );
2020-08-06 15:30:51 -04:00
$memoryCheckEnabled = false ;
}
$dryMode = $input -> getOption ( 'dry' );
2021-12-09 10:52:15 -05:00
$deleteMode = $input -> getOption ( 'delete' );
2020-08-06 15:30:51 -04:00
if ( $dryMode ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'INFO: The migration is run in dry mode and will not modify anything.' );
$output -> writeln ( '' );
2021-12-09 10:52:15 -05:00
} elseif ( $deleteMode ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'WARN: The migration will _DELETE_ old previews.' );
$output -> writeln ( '' );
2020-08-06 15:30:51 -04:00
}
$instanceId = $this -> config -> getSystemValueString ( 'instanceid' );
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'This will migrate all previews from the old preview location to the new one.' );
2020-08-06 15:30:51 -04:00
$output -> writeln ( '' );
$output -> writeln ( 'Fetching previews that need to be migrated …' );
/** @var \OCP\Files\Folder $currentPreviewFolder */
$currentPreviewFolder = $this -> rootFolder -> get ( " appdata_ $instanceId /preview " );
$directoryListing = $currentPreviewFolder -> getDirectoryListing ();
$total = count ( $directoryListing );
/**
* by default there could be 0 - 9 a - f and the old - multibucket folder which are all fine
*/
if ( $total < 18 ) {
2020-08-07 04:34:55 -04:00
$directoryListing = array_filter ( $directoryListing , function ( $dir ) {
2020-08-06 15:30:51 -04:00
if ( $dir -> getName () === 'old-multibucket' ) {
2020-08-07 04:34:55 -04:00
return false ;
2020-08-06 15:30:51 -04:00
}
2020-08-07 04:34:55 -04:00
2020-08-06 15:30:51 -04:00
// a-f can't be a file ID -> removing from migration
if ( preg_match ( '!^[a-f]$!' , $dir -> getName ())) {
2020-08-07 04:34:55 -04:00
return false ;
2020-08-06 15:30:51 -04:00
}
2020-08-07 04:34:55 -04:00
2020-08-06 15:30:51 -04:00
if ( preg_match ( '!^[0-9]$!' , $dir -> getName ())) {
// ignore folders that only has folders in them
if ( $dir instanceof Folder ) {
foreach ( $dir -> getDirectoryListing () as $entry ) {
if ( ! $entry instanceof Folder ) {
2020-08-07 04:34:55 -04:00
return true ;
2020-08-06 15:30:51 -04:00
}
}
2020-08-07 04:34:55 -04:00
return false ;
2020-08-06 15:30:51 -04:00
}
}
2020-08-07 04:34:55 -04:00
return true ;
});
2020-08-06 15:30:51 -04:00
$total = count ( $directoryListing );
}
if ( $total === 0 ) {
2024-08-23 09:10:27 -04:00
$output -> writeln ( 'All previews are already migrated.' );
2020-08-06 15:30:51 -04:00
return 0 ;
}
$output -> writeln ( " A total of $total preview files need to be migrated. " );
2024-08-23 09:10:27 -04:00
$output -> writeln ( '' );
$output -> writeln ( 'The migration will always migrate all previews of a single file in a batch. After each batch the process can be canceled by pressing CTRL-C. This will finish the current batch and then stop the migration. This migration can then just be started and it will continue.' );
2020-08-06 15:30:51 -04:00
if ( $input -> getOption ( 'batch' )) {
$output -> writeln ( 'Batch mode active: migration is started right away.' );
} else {
$helper = $this -> getHelper ( 'question' );
$question = new ConfirmationQuestion ( '<info>Should the migration be started? (y/[n]) </info>' , false );
if ( ! $helper -> ask ( $input , $output , $question )) {
return 0 ;
}
}
// register the SIGINT listener late in here to be able to exit in the early process of this command
pcntl_signal ( SIGINT , [ $this , 'sigIntHandler' ]);
2024-08-23 09:10:27 -04:00
$output -> writeln ( '' );
$output -> writeln ( '' );
2020-08-06 15:30:51 -04:00
$section1 = $output -> section ();
$section2 = $output -> section ();
$progressBar = new ProgressBar ( $section2 , $total );
2024-08-23 09:10:27 -04:00
$progressBar -> setFormat ( '%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% Used Memory: %memory:6s%' );
2020-08-06 15:30:51 -04:00
$time = ( new \DateTime ()) -> format ( 'H:i:s' );
$progressBar -> setMessage ( " $time Starting … " );
$progressBar -> maxSecondsBetweenRedraws ( 0.2 );
$progressBar -> start ();
foreach ( $directoryListing as $oldPreviewFolder ) {
pcntl_signal_dispatch ();
$name = $oldPreviewFolder -> getName ();
$time = ( new \DateTime ()) -> format ( 'H:i:s' );
$section1 -> writeln ( " $time Migrating previews of file with fileId $name … " );
$progressBar -> display ();
if ( $this -> stopSignalReceived ) {
$section1 -> writeln ( " $time Stopping migration … " );
return 0 ;
}
if ( ! $oldPreviewFolder instanceof Folder ) {
$section1 -> writeln ( " Skipping non-folder $name … " );
$progressBar -> advance ();
continue ;
}
if ( $name === 'old-multibucket' ) {
$section1 -> writeln ( " Skipping fallback mount point $name … " );
$progressBar -> advance ();
continue ;
}
if ( in_array ( $name , [ 'a' , 'b' , 'c' , 'd' , 'e' , 'f' ])) {
$section1 -> writeln ( " Skipping hex-digit folder $name … " );
$progressBar -> advance ();
continue ;
}
if ( ! preg_match ( '!^\d+$!' , $name )) {
$section1 -> writeln ( " Skipping non-numeric folder $name … " );
$progressBar -> advance ();
continue ;
}
$newFoldername = Root :: getInternalFolder ( $name );
$memoryUsage = memory_get_usage ();
if ( $memoryCheckEnabled && $memoryUsage > $this -> memoryTreshold ) {
2024-08-23 09:10:27 -04:00
$section1 -> writeln ( '' );
$section1 -> writeln ( '' );
$section1 -> writeln ( '' );
$section1 -> writeln ( ' Stopped process 25 MB before reaching the memory limit to avoid a hard crash.' );
2020-08-06 15:30:51 -04:00
$time = ( new \DateTime ()) -> format ( 'H:i:s' );
$section1 -> writeln ( " $time Reached memory limit and stopped to avoid hard crash. " );
return 1 ;
}
2020-09-17 10:03:34 -04:00
$lockName = 'occ preview:repair lock ' . $oldPreviewFolder -> getId ();
try {
2020-09-17 10:30:11 -04:00
$section1 -> writeln ( " Locking \" $lockName\ " … " , OutputInterface::VERBOSITY_VERBOSE);
2020-09-17 10:03:34 -04:00
$this -> lockingProvider -> acquireLock ( $lockName , ILockingProvider :: LOCK_EXCLUSIVE );
} catch ( LockedException $e ) {
2024-08-23 09:10:27 -04:00
$section1 -> writeln ( ' Skipping because it is locked - another process seems to work on this …' );
2020-09-17 10:03:34 -04:00
continue ;
}
2020-08-06 15:30:51 -04:00
$previews = $oldPreviewFolder -> getDirectoryListing ();
if ( $previews !== []) {
try {
$this -> rootFolder -> get ( " appdata_ $instanceId /preview/ $newFoldername " );
} catch ( NotFoundException $e ) {
2020-09-17 10:30:33 -04:00
$section1 -> writeln ( " Create folder preview/ $newFoldername " , OutputInterface :: VERBOSITY_VERBOSE );
2020-08-06 15:30:51 -04:00
if ( ! $dryMode ) {
$this -> rootFolder -> newFolder ( " appdata_ $instanceId /preview/ $newFoldername " );
}
}
foreach ( $previews as $preview ) {
pcntl_signal_dispatch ();
$previewName = $preview -> getName ();
if ( $preview instanceof Folder ) {
$section1 -> writeln ( " Skipping folder $name / $previewName … " );
$progressBar -> advance ();
continue ;
}
2021-12-09 10:52:15 -05:00
// Execute process
2020-08-06 15:30:51 -04:00
if ( ! $dryMode ) {
2021-12-09 10:52:15 -05:00
// Delete preview instead of moving
if ( $deleteMode ) {
try {
$section1 -> writeln ( " Delete preview/ $name / $previewName " , OutputInterface :: VERBOSITY_VERBOSE );
$preview -> delete ();
} catch ( \Exception $e ) {
2022-04-12 11:55:01 -04:00
$this -> logger -> error ( " Failed to delete preview at preview/ $name / $previewName " , [
'app' => 'core' ,
'exception' => $e ,
]);
2021-12-09 10:52:15 -05:00
}
} else {
try {
$section1 -> writeln ( " Move preview/ $name / $previewName to preview/ $newFoldername " , OutputInterface :: VERBOSITY_VERBOSE );
$preview -> move ( " appdata_ $instanceId /preview/ $newFoldername / $previewName " );
} catch ( \Exception $e ) {
2022-04-12 11:55:01 -04:00
$this -> logger -> error ( " Failed to move preview from preview/ $name / $previewName to preview/ $newFoldername " , [
'app' => 'core' ,
'exception' => $e ,
]);
2021-12-09 10:52:15 -05:00
}
2020-08-06 15:30:51 -04:00
}
}
}
}
2022-04-12 11:55:01 -04:00
2020-08-06 15:30:51 -04:00
if ( $oldPreviewFolder -> getDirectoryListing () === []) {
2020-09-17 10:30:33 -04:00
$section1 -> writeln ( " Delete empty folder preview/ $name " , OutputInterface :: VERBOSITY_VERBOSE );
2020-08-06 15:30:51 -04:00
if ( ! $dryMode ) {
try {
$oldPreviewFolder -> delete ();
} catch ( \Exception $e ) {
2022-04-12 11:55:01 -04:00
$this -> logger -> error ( " Failed to delete empty folder preview/ $name " , [
'app' => 'core' ,
'exception' => $e ,
]);
2020-08-06 15:30:51 -04:00
}
}
}
2020-09-17 10:03:34 -04:00
$this -> lockingProvider -> releaseLock ( $lockName , ILockingProvider :: LOCK_EXCLUSIVE );
2024-08-23 09:10:27 -04:00
$section1 -> writeln ( ' Unlocked' , OutputInterface :: VERBOSITY_VERBOSE );
2020-09-17 10:03:34 -04:00
2020-08-06 15:30:51 -04:00
$section1 -> writeln ( " Finished migrating previews of file with fileId $name … " );
$progressBar -> advance ();
}
$progressBar -> finish ();
2024-08-23 09:10:27 -04:00
$output -> writeln ( '' );
2020-08-06 15:30:51 -04:00
return 0 ;
}
protected function sigIntHandler () {
echo " \n Signal received - will finish the step and then stop the migration. \n \n \n " ;
$this -> stopSignalReceived = true ;
}
}