‪TYPO3CMS  10.4
LostFilesCommand.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Symfony\Component\Console\Command\Command;
21 use Symfony\Component\Console\Input\InputInterface;
22 use Symfony\Component\Console\Input\InputOption;
23 use Symfony\Component\Console\Output\OutputInterface;
24 use Symfony\Component\Console\Style\SymfonyStyle;
31 
35 class ‪LostFilesCommand extends Command
36 {
37 
41  public function ‪configure()
42  {
43  $this
44  ->setDescription('Looking for files in the uploads/ folder which does not have a reference in TYPO3 managed records.')
45  ->setHelp('
46 Assumptions:
47 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
48 - that all contents in the uploads folder are files attached to TCA records and exclusively managed by DataHandler through "group" type fields
49 - index.html, .htaccess files (ignored)
50 - Files found in deleted records are included (otherwise you would see a false list of lost files)
51 
52 The assumptions are not requirements by the TYPO3 API but reflect the de facto implementation of most TYPO3 installations and therefore are a practical approach to clean up the uploads/ or custom folder.
53 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.
54 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.
55 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()
56 Another scenario could of course be de-installation of extensions which managed files in the uploads/ or custom folders.
57 
58 If the option "--dry-run" is not set, the files are then deleted automatically.
59 Warning: First, make sure those files are not used somewhere TYPO3 does not know about! See the assumptions above.
60 
61 If you want to get more detailed information, use the --verbose option.')
62  ->addOption(
63  'exclude',
64  null,
65  InputOption::VALUE_REQUIRED,
66  'Comma-separated list of paths that should be excluded, e.g. "uploads/pics,uploads/media"'
67  )
68  ->addOption(
69  'dry-run',
70  null,
71  InputOption::VALUE_NONE,
72  'If this option is set, the files will not actually be deleted, but just the output which files would be deleted are shown'
73  )
74  ->addOption(
75  'update-refindex',
76  null,
77  InputOption::VALUE_NONE,
78  'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
79  )
80  ->addOption(
81  'custom-path',
82  null,
83  InputOption::VALUE_REQUIRED,
84  'Comma separated list of paths to process. Example: "fileadmin/[path1],fileadmin/[path2],...", if not passed, uploads/ will be used by default.'
85  );
86  }
87 
98  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
99  {
100  // Make sure the _cli_ user is loaded
102 
103  $io = new SymfonyStyle($input, ‪$output);
104  $io->title($this->getDescription());
105 
106  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
107 
108  $this->‪updateReferenceIndex($input, $io);
109 
110  // Find the lost files
111  if ($input->hasOption('exclude') && !empty($input->getOption('exclude'))) {
112  $exclude = $input->getOption('exclude');
113  $exclude = is_string($exclude) ? $exclude : '';
114  $excludedPaths = ‪GeneralUtility::trimExplode(',', $exclude, true);
115  } else {
116  $excludedPaths = [];
117  }
118 
119  // Use custom-path
120  $customPaths = '';
121  if ($input->hasOption('custom-path') && !empty($input->getOption('custom-path'))) {
122  $customPaths = $input->getOption('custom-path');
123  $customPaths = is_string($customPaths) ? $customPaths : '';
124  }
125 
126  $lostFiles = $this->‪findLostFiles($excludedPaths, $customPaths);
127 
128  if (count($lostFiles)) {
129  if (!$io->isQuiet()) {
130  $io->note('Found ' . count($lostFiles) . ' lost files, ready to be deleted.');
131  if ($io->isVerbose()) {
132  $io->listing($lostFiles);
133  }
134  }
135 
136  // Delete them
137  $this->‪deleteLostFiles($lostFiles, $dryRun, $io);
138 
139  $io->success('Deleted ' . count($lostFiles) . ' lost files.');
140  } else {
141  $io->success('Nothing to do, no lost files found');
142  }
143  return 0;
144  }
145 
155  protected function ‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
156  {
157  // Check for reference index to update
158  $io->note('Finding lost files managed by TYPO3 requires a clean reference index (sys_refindex)');
159  $updateReferenceIndex = false;
160  if ($input->hasOption('update-refindex') && $input->getOption('update-refindex')) {
161  $updateReferenceIndex = true;
162  } elseif ($input->isInteractive()) {
163  $updateReferenceIndex = $io->confirm('Should the reference index be updated right now?', false);
164  }
165 
166  // Update the reference index
167  if ($updateReferenceIndex) {
168  $progressListener = GeneralUtility::makeInstance(ReferenceIndexProgressListener::class);
169  $progressListener->initialize($io);
170  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
171  $io->section('Reference Index is now being updated');
172  $referenceIndex->updateIndex(false, $progressListener);
173  } else {
174  $io->writeln('Reference index is assumed to be up to date, continuing.');
175  }
176  }
177 
185  protected function ‪findLostFiles($excludedPaths = [], $customPaths = ''): array
186  {
187  $lostFiles = [];
188 
189  // Get all files
190  $files = [];
191  if (!empty($customPaths)) {
192  $customPaths = ‪GeneralUtility::trimExplode(',', $customPaths, true);
193  foreach ($customPaths as $customPath) {
194  if (false === realpath(‪Environment::getPublicPath() . '/' . $customPath)
195  || !GeneralUtility::isFirstPartOfStr((string)realpath(‪Environment::getPublicPath() . '/' . $customPath), (string)realpath(‪Environment::getPublicPath()))) {
196  throw new \Exception('The path: "' . $customPath . '" is invalid', 1450086736);
197  }
198  $files = GeneralUtility::getAllFilesAndFoldersInPath($files, ‪Environment::getPublicPath() . '/' . $customPath);
199  }
200  } else {
201  $files = GeneralUtility::getAllFilesAndFoldersInPath($files, ‪Environment::getPublicPath() . '/uploads/');
202  }
203 
204  $files = GeneralUtility::removePrefixPathFromList($files, ‪Environment::getPublicPath() . '/');
205 
206  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
207  ->getQueryBuilderForTable('sys_refindex');
208 
209  // Traverse files and for each, look up if its found in the reference index.
210  foreach ($files as $key => $value) {
211 
212  // First, allow "index.html", ".htaccess" files since they are often used for good reasons
213  if (substr($value, -11) === '/index.html' || substr($value, -10) === '/.htaccess') {
214  continue;
215  }
216 
217  $fileIsInExcludedPath = false;
218  foreach ($excludedPaths as $exclPath) {
219  if (GeneralUtility::isFirstPartOfStr($value, $exclPath)) {
220  $fileIsInExcludedPath = true;
221  break;
222  }
223  }
224 
225  if ($fileIsInExcludedPath) {
226  continue;
227  }
228 
229  // Looking for a reference from a field which is NOT a soft reference (thus, only fields with a proper TCA/Flexform configuration)
230  $queryBuilder
231  ->select('hash')
232  ->from('sys_refindex')
233  ->where(
234  $queryBuilder->expr()->eq(
235  'ref_table',
236  $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
237  ),
238  $queryBuilder->expr()->eq(
239  'ref_string',
240  $queryBuilder->createNamedParameter($value, \PDO::PARAM_STR)
241  ),
242  $queryBuilder->expr()->eq(
243  'softref_key',
244  $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
245  )
246  )
247  ->orderBy('sorting', 'DESC')
248  ->execute();
249 
250  $rowCount = $queryBuilder->count('hash')->execute()->fetchColumn(0);
251  // We conclude that the file is lost
252  if ($rowCount === 0) {
253  $lostFiles[] = $value;
254  }
255  }
256 
257  return $lostFiles;
258  }
259 
267  protected function ‪deleteLostFiles(array $lostFiles, bool $dryRun, SymfonyStyle $io)
268  {
269  foreach ($lostFiles as $lostFile) {
270  $absoluteFileName = GeneralUtility::getFileAbsFileName($lostFile);
271  if ($io->isVeryVerbose()) {
272  $io->writeln('Deleting file "' . $absoluteFileName . '"');
273  }
274  if (!$dryRun) {
275  if ($absoluteFileName && @is_file($absoluteFileName)) {
276  unlink($absoluteFileName);
277  if (!$io->isQuiet()) {
278  $io->writeln('Permanently deleted file record "' . $absoluteFileName . '".');
279  }
280  } else {
281  $io->error('File "' . $absoluteFileName . '" was not found!');
282  }
283  }
284  }
285  }
286 }
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand\findLostFiles
‪array findLostFiles($excludedPaths=[], $customPaths='')
Definition: LostFilesCommand.php:185
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand\configure
‪configure()
Definition: LostFilesCommand.php:41
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:180
‪TYPO3\CMS\Backend\Command\ProgressListener\ReferenceIndexProgressListener
Definition: ReferenceIndexProgressListener.php:30
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:48
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:607
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand\updateReferenceIndex
‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
Definition: LostFilesCommand.php:155
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand\execute
‪int execute(InputInterface $input, OutputInterface $output)
Definition: LostFilesCommand.php:98
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand\deleteLostFiles
‪deleteLostFiles(array $lostFiles, bool $dryRun, SymfonyStyle $io)
Definition: LostFilesCommand.php:267
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static string[] trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:1059
‪$output
‪$output
Definition: annotationChecker.php:119
‪TYPO3\CMS\Lowlevel\Command\LostFilesCommand
Definition: LostFilesCommand.php:36
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:40
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:66
‪TYPO3\CMS\Lowlevel\Command
Definition: CleanFlexFormsCommand.php:18
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46