2 declare(strict_types = 1);
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Symfony\Component\Console\Style\SymfonyStyle;
42 ->setDescription(
'Looking for files in the uploads/ folder which does not have a reference in TYPO3 managed records.')
45 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
46 - that all contents in the uploads folder are files attached to TCA records and exclusively managed by DataHandler through "group" type fields
47 - index.html, .htaccess files and RTEmagic* image files (ignored)
48 - Files found in deleted records are included (otherwise you would see a false list of lost files)
50 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/ or costum folder.
51 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.
52 Under such circumstances there should theoretically be no lost files in the uploads/ or custom folder since DataHandler should have managed relations automatically including adding and deleting files.
53 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 FlexFormTools->getDataStructureIdentifier()
54 Another scenario could of course be de-installation of extensions which managed files in the uploads/ or custom folders.
56 If the option "--dry-run" is not set, the files are then deleted automatically.
57 Warning: First, make sure those files are not used somewhere TYPO3 does not know about! See the assumptions above.
59 If you want to get more detailed information, use the --verbose option.')
63 InputOption::VALUE_REQUIRED,
64 'Comma-separated list of paths that should be excluded, e.g. "uploads/pics,uploads/media"'
69 InputOption::VALUE_NONE,
70 'If this option is set, the files will not actually be deleted, but just the output which files would be deleted are shown'
75 InputOption::VALUE_NONE,
76 'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
81 InputOption::VALUE_REQUIRED,
82 'Comma separated list of paths to process. Example: "fileadmin/[path1],fileadmin/[path2],...", if not passed, uploads/ will be used by default.'
100 $io =
new SymfonyStyle($input,
$output);
101 $io->title($this->getDescription());
103 $dryRun = $input->hasOption(
'dry-run') && $input->getOption(
'dry-run') !=
false ? true :
false;
108 if ($input->hasOption(
'exclude') && !empty($input->getOption(
'exclude'))) {
109 $excludedPaths = GeneralUtility::trimExplode(
',', $input->getOption(
'exclude'),
true);
116 if ($input->hasOption(
'custom-path') && !empty($input->getOption(
'custom-path'))) {
117 $customPaths = $input->getOption(
'custom-path');
122 if (count($lostFiles)) {
123 if (!$io->isQuiet()) {
124 $io->note(
'Found ' . count($lostFiles) .
' lost files, ready to be deleted.');
125 if ($io->isVerbose()) {
126 $io->listing($lostFiles);
133 $io->success(
'Deleted ' . count($lostFiles) .
' lost files.');
135 $io->success(
'Nothing to do, no lost files found');
151 $io->note(
'Finding lost files managed by TYPO3 requires a clean reference index (sys_refindex)');
152 $updateReferenceIndex =
false;
153 if ($input->hasOption(
'update-refindex') && $input->getOption(
'update-refindex')) {
154 $updateReferenceIndex =
true;
155 } elseif ($input->isInteractive()) {
156 $updateReferenceIndex = $io->confirm(
'Should the reference index be updated right now?',
false);
160 if ($updateReferenceIndex) {
161 $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
162 $referenceIndex->updateIndex(
false, !$io->isQuiet());
164 $io->writeln(
'Reference index is assumed to be up to date, continuing.');
181 if (!empty($customPaths)) {
182 $customPaths = GeneralUtility::trimExplode(
',', $customPaths,
true);
183 foreach ($customPaths as $customPath) {
186 throw new \Exception(
'The path: "' . $customPath .
'" is invalid', 1450086736);
196 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
197 ->getQueryBuilderForTable(
'sys_refindex');
200 foreach ($files as $key => $value) {
203 if (substr($value, -11) ===
'/index.html' || substr($value, -10) ===
'/.htaccess') {
212 $fileIsInExcludedPath =
false;
213 foreach ($excludedPaths as $exclPath) {
214 if (GeneralUtility::isFirstPartOfStr($value, $exclPath)) {
215 $fileIsInExcludedPath =
true;
220 if ($fileIsInExcludedPath) {
227 ->from(
'sys_refindex')
229 $queryBuilder->expr()->eq(
231 $queryBuilder->createNamedParameter(
'_FILE', \PDO::PARAM_STR)
233 $queryBuilder->expr()->eq(
235 $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
237 $queryBuilder->expr()->eq(
239 $queryBuilder->createNamedParameter(
'', \PDO::PARAM_STR)
242 ->orderBy(
'sorting',
'DESC')
245 $rowCount = $queryBuilder->count(
'hash')->execute()->fetchColumn(0);
247 if ($rowCount === 0) {
248 $lostFiles[] = $value;
264 foreach ($lostFiles as $lostFile) {
265 $absoluteFileName = GeneralUtility::getFileAbsFileName($lostFile);
266 if ($io->isVeryVerbose()) {
267 $io->writeln(
'Deleting file "' . $absoluteFileName .
'"');
270 if ($absoluteFileName && @is_file($absoluteFileName)) {
271 unlink($absoluteFileName);
272 if (!$io->isQuiet()) {
273 $io->writeln(
'Permanently deleted file record "' . $absoluteFileName .
'".');
276 $io->error(
'File "' . $absoluteFileName .
'" was not found!');