[!!!][TASK] Migrate LostFilesCommand to Symfony Console 87/50487/6
authorBenni Mack <benni@typo3.org>
Thu, 3 Nov 2016 06:13:26 +0000 (07:13 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Sat, 5 Nov 2016 12:22:48 +0000 (13:22 +0100)
The EXT:lowlevel command "LostFiles" searched through
uploads/* and checks if any files are not in use by TYPO3 by
checking against the reference index.

The command was previously available under
./typo3/cli_dispatch lowlevel_cleaner lost_files

and is now called via
./typo3/sysext/core/bin/typo3 cleanup:lostfiles

and allows the following options:
--dry-run - do not delete the files, but just print them
--exclude=uploads/pics/,uploads/downloads/ -- skip certain folders and files
--update-refindex - update the reference index, do not ask the user

Resolves: #78552
Releases: master
Change-Id: I1f20c4c2f47d1dd13f8f32433e7e0d0666518d5a
Reviewed-on: https://review.typo3.org/50487
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Breaking-78552-LowlevelLostFilesCommandParametersChanged.rst [new file with mode: 0644]
typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php [new file with mode: 0644]
typo3/sysext/lowlevel/Classes/LostFilesCommand.php [deleted file]
typo3/sysext/lowlevel/Configuration/Commands.php
typo3/sysext/lowlevel/ext_localconf.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-78552-LowlevelLostFilesCommandParametersChanged.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-78552-LowlevelLostFilesCommandParametersChanged.rst
new file mode 100644 (file)
index 0000000..5e852eb
--- /dev/null
@@ -0,0 +1,42 @@
+.. include:: ../../Includes.txt
+
+===============================================================
+Breaking: #78552 - Lowlevel LostFilesCommand parameters changed
+===============================================================
+
+See :issue:`78552`
+
+Description
+===========
+
+The existing CLI command within EXT:lowlevel for detecting and removing files within uploads/ which are not referenced by TYPO3
+has been migrated to a Symfony Console command.
+
+The previously command available via `./typo3/cli_dispatch.phpsh lowlevel_cleaner lost_files` is now available via
+`./typo3/sysext/core/bin/typo3 cleanup:lostfiles` and allows the following CLI options to be set:
+
+`--update-refindex` - updates the reference index before scanning for lost files. If not set, the user is asked if the task should be run
+`--exclude=uploads/mypics/,uploads/psa` - a list of paths of files to exclude within uploads/
+`--dry-run` - do not delete the files but only list the files that are not connected to the TYPO3 system anymore
+
+The PHP class of the old CLI command `TYPO3\CMS\Lowlevel\LostFilesCommand` has been removed.
+
+
+Impact
+======
+
+Calling the old CLI command `./typo3/cli_dispatch.phpsh lowlevel_cleaner lost_files` will result in an error message.
+
+
+Affected Installations
+======================
+
+Any TYPO3 instances using the lowlevel cleaner for finding and deleting lost files.
+
+
+Migration
+=========
+
+Update the CLI call on your servers to the new command line and available options as shown above.
+
+.. index:: CLI
diff --git a/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php b/typo3/sysext/lowlevel/Classes/Command/LostFilesCommand.php
new file mode 100644 (file)
index 0000000..c107cad
--- /dev/null
@@ -0,0 +1,252 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Lowlevel\Command;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\ReferenceIndex;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Finds files within uploads/ which are not needed anymore
+ */
+class LostFilesCommand extends Command
+{
+
+    /**
+     * Configure the command by defining the name, options and arguments
+     */
+    public function configure()
+    {
+        $this
+            ->setDescription('Looking for files in the uploads/ folder which does not have a reference in TYPO3 managed records.')
+            ->setHelp('
+Assumptions:
+- a perfect integrity of the reference index table (always update the reference index table before using this tool!)
+- that all contents in the uploads folder are files attached to TCA records and exclusively managed by DataHandler through "group" type fields
+- index.html, .htaccess files and RTEmagic* image files (ignored)
+- Files found in deleted records are included (otherwise you would see a false list of lost files)
+
+The assumptions are not requirements by the TYPO3 API but reflects the de facto implementation of most TYPO3 installations and therefore a practical approach to cleaning up the uploads/ folder.
+Therefore, if all "group" type fields in TCA and flexforms are positioned inside the uploads/ folder and if no files inside are managed manually it should be safe to clean out files with no relations found in the system.
+Under such circumstances there should theoretically be no lost files in the uploads/ folder since DataHandler should have managed relations automatically including adding and deleting files.
+However, there is at least one reason known to why files might be found lost and that is when FlexForms are used. In such a case a change of/in the Data Structure XML (or the ability of the system to find the Data Structure definition!) used for the flexform could leave lost files behind. This is not unlikely to happen when records are deleted. More details can be found in a note to the function TYPO3\\CMS\\Backend\\Utility\\BackendUtility::getFlexFormDS()
+Another scenario could of course be de-installation of extensions which managed files in the uploads/ folders.
+
+If the option "--dry-run" is not set, the files are then deleted automatically.
+Warning: First, make sure those files are not used somewhere TYPO3 does not know about! See the assumptions above.
+
+If you want to get more detailed information, use the --verbose option.')
+            ->addOption(
+                'exclude',
+                null,
+                InputOption::VALUE_REQUIRED,
+                'Comma-separated list of paths that should be excluded, e.g. "uploads/pics,uploads/media"'
+            )
+            ->addOption(
+                'dry-run',
+                null,
+                InputOption::VALUE_NONE,
+                'If this option is set, the files will not actually be deleted, but just the output which files would be deleted are shown'
+            )
+            ->addOption(
+                'update-refindex',
+                null,
+                InputOption::VALUE_NONE,
+                'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
+            );
+    }
+
+    /**
+     * Executes the command to
+     * - optionally update the reference index (to have clean data)
+     * - find files within uploads/* which are not connected to the reference index
+     * - remove these files if --dry-run is not set
+     *
+     * @param InputInterface $input
+     * @param OutputInterface $output
+     *
+     * @return void
+     */
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $io = new SymfonyStyle($input, $output);
+        $io->title($this->getDescription());
+
+        $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
+
+        $this->updateReferenceIndex($input, $io);
+
+        // Find the lost files
+        if ($input->hasOption('exclude') && !empty($input->getOption('exclude'))) {
+            $excludedPaths = GeneralUtility::trimExplode(',', $input->getOption('exclude'), true);
+        } else {
+            $excludedPaths = [];
+        }
+        $lostFiles = $this->findLostFiles($excludedPaths);
+
+        if (count($lostFiles)) {
+            if (!$io->isQuiet()) {
+                $io->note('Found ' . count($lostFiles) . ' lost files, ready to be deleted.');
+                if ($io->isVerbose()) {
+                    $io->listing($lostFiles);
+                }
+            }
+
+            // Delete them
+            $this->deleteLostFiles($lostFiles, $dryRun, $io);
+
+            $io->success('Deleted ' . count($lostFiles) . ' lost files.');
+        } else {
+            $io->success('Nothing to do, no lost files found');
+        }
+    }
+
+    /**
+     * Function to update the reference index
+     * - if the option --update-refindex is set, do it
+     * - otherwise, if in interactive mode (not having -n set), ask the user
+     * - otherwise assume everything is fine
+     *
+     * @param InputInterface $input holds information about entered parameters
+     * @param SymfonyStyle $io necessary for outputting information
+     * @return void
+     */
+    protected function updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
+    {
+        // Check for reference index to update
+        $io->note('Finding lost files managed by TYPO3 requires a clean reference index (sys_refindex)');
+        $updateReferenceIndex = false;
+        if ($input->hasOption('update-refindex') && $input->getOption('update-refindex')) {
+            $updateReferenceIndex = true;
+        } elseif ($input->isInteractive()) {
+            $updateReferenceIndex = $io->confirm('Should the reference index be updated right now?', false);
+        }
+
+        // Update the reference index
+        if ($updateReferenceIndex) {
+            $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
+            $referenceIndex->updateIndex(false, !$io->isQuiet());
+        } else {
+            $io->writeln('Reference index is assumed to be up to date, continuing.');
+        }
+    }
+
+    /**
+     * Find lost files in uploads/ folder
+     *
+     * @param array $excludedPaths list of paths to be excluded, can be uploads/pics/
+     * @return array an array of files (relative to PATH_site) that are not connected
+     */
+    protected function findLostFiles($excludedPaths = []): array
+    {
+        $lostFiles = [];
+
+        // Get all files
+        $files = [];
+        $files = GeneralUtility::getAllFilesAndFoldersInPath($files, PATH_site . 'uploads/');
+        $files = GeneralUtility::removePrefixPathFromList($files, PATH_site);
+
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getQueryBuilderForTable('sys_refindex');
+
+        // Traverse files and for each, look up if its found in the reference index.
+        foreach ($files as $key => $value) {
+
+            // First, allow "index.html", ".htaccess" files since they are often used for good reasons
+            if (substr($value, -11) === '/index.html' || substr($value, -10) === '/.htaccess') {
+                continue;
+            }
+
+            // If the file is a RTEmagic-image name and if so, we allow it
+            if (preg_match('/^RTEmagic[P|C]_/', basename($value))) {
+                continue;
+            }
+
+            $fileIsInExcludedPath = false;
+            foreach ($excludedPaths as $exclPath) {
+                if (GeneralUtility::isFirstPartOfStr($value, $exclPath)) {
+                    $fileIsInExcludedPath = true;
+                    break;
+                }
+            }
+
+            if ($fileIsInExcludedPath) {
+                continue;
+            }
+
+            // Looking for a reference from a field which is NOT a soft reference (thus, only fields with a proper TCA/Flexform configuration)
+            $result = $queryBuilder
+                ->select('hash')
+                ->from('sys_refindex')
+                ->where(
+                    $queryBuilder->expr()->eq(
+                        'ref_table',
+                        $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'ref_string',
+                        $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'softref_key',
+                        $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
+                    )
+                )
+                ->orderBy('sorting', 'DESC')
+                ->execute();
+
+            // We conclude that the file is lost
+            if ($result->rowCount() === 0) {
+                $lostFiles[] = $value;
+            }
+        }
+
+        return $lostFiles;
+    }
+
+    /**
+     * Removes given files from the uploads/ folder
+     *
+     * @param array $lostFiles Contains the lost files found
+     * @param bool $dryRun if set, the files are just displayed, but not deleted
+     * @param SymfonyStyle $io the IO object for output
+     * @return void
+     */
+    protected function deleteLostFiles(array $lostFiles, bool $dryRun, SymfonyStyle $io)
+    {
+        foreach ($lostFiles as $lostFile) {
+            $absoluteFileName = GeneralUtility::getFileAbsFileName($lostFile);
+            if ($io->isVeryVerbose()) {
+                $io->writeln('Deleting file "' . $absoluteFileName . '"');
+            }
+            if (!$dryRun) {
+                if ($absoluteFileName && @is_file($absoluteFileName)) {
+                    unlink($absoluteFileName);
+                    if (!$io->isQuiet()) {
+                        $io->writeln('Permanently deleted file record "' . $absoluteFileName . '".');
+                    }
+                } else {
+                    $io->error('File "' . $absoluteFileName . '" was not found!');
+                }
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/lowlevel/Classes/LostFilesCommand.php b/typo3/sysext/lowlevel/Classes/LostFilesCommand.php
deleted file mode 100644 (file)
index e7b2fa5..0000000
+++ /dev/null
@@ -1,187 +0,0 @@
-<?php
-namespace TYPO3\CMS\Lowlevel;
-
-/*
- * This file is part of the TYPO3 CMS project.
- *
- * It is free software; you can redistribute it and/or modify it under
- * the terms of the GNU General Public License, either version 2
- * of the License, or any later version.
- *
- * For the full copyright and license information, please read the
- * LICENSE.txt file that was distributed with this source code.
- *
- * The TYPO3 project - inspiring people to share!
- */
-
-use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Looking for Lost files
- */
-class LostFilesCommand extends CleanerCommand
-{
-    /**
-     * @var bool
-     */
-    public $checkRefIndex = true;
-
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        parent::__construct();
-        $this->cli_options[] = ['--excludePath [path-list]', 'Comma separated list of paths to exclude. Example: "uploads/[path1],uploads/[path2],..."'];
-        // Setting up help:
-        $this->cli_help['name'] = 'lost_files -- Looking for files in the uploads/ folder which does not have a reference in TYPO3 managed records.';
-        $this->cli_help['description'] = trim('
-Assumptions:
-- a perfect integrity of the reference index table (always update the reference index table before using this tool!)
-- that all contents in the uploads folder are files attached to TCA records and exclusively managed by TCEmain through "group" type fields
-- exceptions are: index.html and .htaccess files (ignored)
-- exceptions are: RTEmagic* image files (ignored)
-- files found in deleted records are included (otherwise you would see a false list of lost files)
-
-The assumptions are not requirements by the TYPO3 API but reflects the de facto implementation of most TYPO3 installations and therefore a practical approach to cleaning up the uploads/ folder.
-Therefore, if all "group" type fields in TCA and flexforms are positioned inside the uploads/ folder and if no files inside are managed manually it should be safe to clean out files with no relations found in the system.
-Under such circumstances there should theoretically be no lost files in the uploads/ folder since TCEmain should have managed relations automatically including adding and deleting files.
-However, there is at least one reason known to why files might be found lost and that is when FlexForms are used. In such a case a change of/in the Data Structure XML (or the ability of the system to find the Data Structure definition!) used for the flexform could leave lost files behind. This is not unlikely to happen when records are deleted. More details can be found in a note to the function TYPO3\\CMS\\Backend\\Utility\\BackendUtility::getFlexFormDS()
-Another scenario could of course be de-installation of extensions which managed files in the uploads/ folders.
-
-Automatic Repair of Errors:
-- Simply delete lost files (Warning: First, make sure those files are not used somewhere TYPO3 does not know about! See the assumptions above).
-');
-        $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner lost_files -s -r
-Will report lost files.';
-    }
-
-    /**
-     * Find lost files in uploads/ folder
-     * FIX METHOD: Simply delete the file...
-     *
-     * @todo Add parameter to exclude filepath
-     * @todo Add parameter to list more file names/patterns to ignore
-     * @todo Add parameter to include RTEmagic images
-     *
-     * @return array
-     */
-    public function main()
-    {
-        // Initialize result array:
-        $resultArray = [
-            'message' => $this->cli_help['name'] . LF . LF . $this->cli_help['description'],
-            'headers' => [
-                'managedFiles' => ['Files related to TYPO3 records and managed by TCEmain', 'These files you definitely want to keep.', 0],
-                'ignoredFiles' => ['Ignored files (index.html, .htaccess etc.)', 'These files are allowed in uploads/ folder', 0],
-                'RTEmagicFiles' => ['RTE magic images - those found (and ignored)', 'These files are also allowed in some uploads/ folders as RTEmagic images.', 0],
-                'lostFiles' => ['Lost files - those you can delete', 'You can delete these files!', 3],
-                'warnings' => ['Warnings picked up', '', 2]
-            ],
-            'managedFiles' => [],
-            'ignoredFiles' => [],
-            'RTEmagicFiles' => [],
-            'lostFiles' => [],
-            'warnings' => []
-        ];
-        // Get all files:
-        $fileArr = [];
-        $fileArr = GeneralUtility::getAllFilesAndFoldersInPath($fileArr, PATH_site . 'uploads/');
-        $fileArr = GeneralUtility::removePrefixPathFromList($fileArr, PATH_site);
-        $excludePaths = GeneralUtility::trimExplode(',', $this->cli_argValue('--excludePath', 0), true);
-        // Traverse files and for each, look up if its found in the reference index.
-        foreach ($fileArr as $key => $value) {
-            $include = true;
-            foreach ($excludePaths as $exclPath) {
-                if (GeneralUtility::isFirstPartOfStr($value, $exclPath)) {
-                    $include = false;
-                }
-            }
-            $shortKey = GeneralUtility::shortMD5($value);
-            if ($include) {
-                // First, allow "index.html", ".htaccess" files since they are often used for good reasons
-                if (substr($value, -11) === '/index.html' || substr($value, -10) === '/.htaccess') {
-                    unset($fileArr[$key]);
-                    $resultArray['ignoredFiles'][$shortKey] = $value;
-                } else {
-                    // Looking for a reference from a field which is NOT a soft reference (thus, only fields with a proper TCA/Flexform configuration)
-                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                        ->getQueryBuilderForTable('sys_refindex');
-
-                    $result = $queryBuilder
-                        ->select('*')
-                        ->from('sys_refindex')
-                        ->where(
-                            $queryBuilder->expr()->eq(
-                                'ref_table',
-                                $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
-                            ),
-                            $queryBuilder->expr()->eq(
-                                'ref_string',
-                                $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
-                            ),
-                            $queryBuilder->expr()->eq(
-                                'softref_key',
-                                $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
-                            )
-                        )
-                        ->orderBy('sorting', 'DESC')
-                        ->execute();
-
-                    // If found, unset entry:
-                    if ($result->rowCount()) {
-                        unset($fileArr[$key]);
-                        $resultArray['managedFiles'][$shortKey] = $value;
-                        if ($result->rowCount() > 1) {
-                            $resultArray['warnings'][$shortKey] = 'Warning: File "' . $value . '" had ' . $result->rowCount() . ' references from group-fields, should have only one!';
-                        }
-                    } else {
-                        // When here it means the file was not found. So we test if it has a RTEmagic-image name and if so, we allow it:
-                        if (preg_match('/^RTEmagic[P|C]_/', basename($value))) {
-                            unset($fileArr[$key]);
-                            $resultArray['RTEmagicFiles'][$shortKey] = $value;
-                        } else {
-                            // We conclude that the file is lost...:
-                            unset($fileArr[$key]);
-                            $resultArray['lostFiles'][$shortKey] = $value;
-                        }
-                    }
-                }
-            }
-        }
-        asort($resultArray['ignoredFiles']);
-        asort($resultArray['managedFiles']);
-        asort($resultArray['RTEmagicFiles']);
-        asort($resultArray['lostFiles']);
-        asort($resultArray['warnings']);
-        // $fileArr variable should now be empty with all contents transferred to the result array keys.
-        return $resultArray;
-    }
-
-    /**
-     * Mandatory autofix function
-     * Will run auto-fix on the result array. Echos status during processing.
-     *
-     * @param array $resultArray Result array from main() function
-     * @return void
-     */
-    public function main_autoFix($resultArray)
-    {
-        foreach ($resultArray['lostFiles'] as $key => $value) {
-            $absFileName = GeneralUtility::getFileAbsFileName($value);
-            echo 'Deleting file: "' . $absFileName . '": ';
-            if ($bypass = $this->cli_noExecutionCheck($absFileName)) {
-                echo $bypass;
-            } else {
-                if ($absFileName && @is_file($absFileName)) {
-                    unlink($absFileName);
-                    echo 'DONE';
-                } else {
-                    echo '     ERROR: File "' . $absFileName . '" was not found!';
-                }
-            }
-            echo LF;
-        }
-    }
-}
index 81e9b02..5c9e707 100644 (file)
@@ -10,6 +10,11 @@ return [
     'syslog:list' => [
         'class' => \TYPO3\CMS\Lowlevel\Command\ListSysLogCommand::class
     ],
     'syslog:list' => [
         'class' => \TYPO3\CMS\Lowlevel\Command\ListSysLogCommand::class
     ],
+    'cleanup:lostfiles' => [
+        'class' => \TYPO3\CMS\Lowlevel\Command\LostFilesCommand::class,
+        // needed for updating the reference index (optional)
+        'user' => '_cli_lowlevel'
+    ],
     'cleanup:deletedrecords' => [
         'class' => \TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand::class,
         'user' => '_cli_lowlevel'
     'cleanup:deletedrecords' => [
         'class' => \TYPO3\CMS\Lowlevel\Command\DeletedRecordsCommand::class,
         'user' => '_cli_lowlevel'
index 920a5f8..2728298 100644 (file)
@@ -15,5 +15,6 @@ if (TYPO3_MODE === 'BE') {
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['double_files'] = [\TYPO3\CMS\Lowlevel\DoubleFilesCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['rte_images'] = [\TYPO3\CMS\Lowlevel\RteImagesCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['lost_files'] = [\TYPO3\CMS\Lowlevel\LostFilesCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['double_files'] = [\TYPO3\CMS\Lowlevel\DoubleFilesCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['rte_images'] = [\TYPO3\CMS\Lowlevel\RteImagesCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['lost_files'] = [\TYPO3\CMS\Lowlevel\LostFilesCommand::class];
+    $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['orphan_records'] = [\TYPO3\CMS\Lowlevel\OrphanRecordsCommand::class];
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['versions'] = [\TYPO3\CMS\Lowlevel\VersionsCommand::class];
 }
     $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules']['versions'] = [\TYPO3\CMS\Lowlevel\VersionsCommand::class];
 }