Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix removal of still used files #27

Merged
merged 1 commit into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 79 additions & 35 deletions Classes/Service/Cleanup/Files.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,44 +135,13 @@ private function markFileReferencesDeletedIfForeignRecordIsMissing(): void

private function deleteFilesWithoutProperReference(): void
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
->select('file.identifier', 'file.storage', 'file.uid')
->addSelectLiteral('SUM(' . $queryBuilder->expr()->eq('reference.deleted', 1) . ') AS deleted_sum')
->from('sys_file', 'file')
->leftJoin(
'file',
'sys_file_reference',
'reference',
'reference.uid_local = file.uid'
)
->where($queryBuilder->expr()->like(
'reference.tablenames',
$queryBuilder->createNamedParameter('tx_events_domain_model_%')
))
->orWhere($queryBuilder->expr()->eq(
'reference.tablenames',
$queryBuilder->createNamedParameter('')
))
->groupBy('file.uid')
->having(
$queryBuilder->expr()->eq(
'deleted_sum',
$queryBuilder->expr()->count('*')
)
)
;
/** @var array{int: array{storage: int, identifier: string, uid: int}} $filesToDelete */
$filesToDelete = $queryBuilder->execute()->fetchAll();
$filesToDelete = $this->filterPotentialFilesToDelete($this->getPotentialFilesToDelete());

$uidsToRemove = [];
foreach ($filesToDelete as $fileToDelete) {
$this->deleteFromFal((int)$fileToDelete['storage'], (string)$fileToDelete['identifier']);
$uidsToRemove[] = (int)$fileToDelete['uid'];
foreach ($filesToDelete as $file) {
$this->deleteFromFal((int)$file['storage'], (string)$file['identifier']);
}

$this->deleteFromDb(...$uidsToRemove);
$this->deleteFromDb(...array_keys($filesToDelete));
}

private function deleteFromFal(int $storageUid, string $filePath): void
Expand Down Expand Up @@ -228,4 +197,79 @@ private function deleteReferences(): void
;
$queryBuilder->execute();
}

/**
* @return array<int, array{storage: int, identifier: string}> Index is file uid.
*/
private function getPotentialFilesToDelete(): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
->select('file.uid', 'file.storage', 'file.identifier')
->from('sys_file', 'file')
->leftJoin(
'file',
'sys_file_reference',
'reference',
'reference.uid_local = file.uid'
)
->where($queryBuilder->expr()->like(
'reference.tablenames',
$queryBuilder->createNamedParameter('tx_events_domain_model_%')
))
->orWhere($queryBuilder->expr()->eq(
'reference.tablenames',
$queryBuilder->createNamedParameter('')
))
->groupBy('file.uid')
;

return $queryBuilder->execute()->fetchAllAssociativeIndexed();
}

/**
* @param array<int, array{storage: int, identifier: string}> $files
* @return array<int, array{storage: int, identifier: string}> Index is file uid.
*/
private function filterPotentialFilesToDelete(array $files): array
{
$filesToDelete = [];
$filesToKeep = [];

$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
->select('*')
->from('sys_file_reference', 'reference')
->where($queryBuilder->expr()->in(
'uid_local',
$queryBuilder->createNamedParameter(array_keys($files), Connection::PARAM_INT_ARRAY)
))
;

foreach ($queryBuilder->execute() as $reference) {
$file = [];
$fileUid = (int) $reference['uid_local'];

if (
(
str_starts_with($reference['tablenames'], 'tx_events_domain_model_')
|| $reference['tablenames'] === ''
) && $reference['deleted'] == 1
) {
$file = $files[$fileUid] ?? [];
} else {
$filesToKeep[$fileUid] = $fileUid;
}

if ($file === []) {
continue;
}

$filesToDelete[$fileUid] = $file;
}

return array_diff_key($filesToDelete, $filesToKeep);
}
}
5 changes: 5 additions & 0 deletions Documentation/Changelog/3.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ Fixes
The time range where this can happen is now reduced as slugs for each event and
date is generated right after saving each of them.

* Improve deletion of files and their relations.
The used database query didn't work as expected and could result in data loss.
There are now two database queries and the logic is moved to PHP.
Furthermore, the test cases were extended with another situation.

Tasks
-----

Expand Down
61 changes: 61 additions & 0 deletions Tests/Functional/Cleanup/Fixtures/RemovePastTestDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
'uid' => '1',
'title' => 'Root page',
'slug' => '1',
'media' => '1',
],
[
'pid' => '1',
Expand Down Expand Up @@ -184,6 +185,26 @@
'identifier_hash' => '475768e491580fb8b74ed36c2b1aaf619ca5e11d',
'folder_hash' => 'b4ab666a114d9905a50606d1837b74d952dfd90f',
],
[
'uid' => '6',
'pid' => '0',
'tstamp' => '1371467047',
'type' => '2',
'storage' => '1',
'identifier' => '/user_uploads/example-events-image-used-somewhere-else.gif',
'extension' => 'gif',
'mime_type' => 'image/gif',
'name' => 'example-events-image-used-somewhere-else.gif',
'sha1' => '359ae0fb420fe8afe1a8b8bc5e46d75090a826b9',
'size' => '637',
'creation_date' => '1370877201',
'modification_date' => '1369407629',
'last_indexed' => '0',
'missing' => '0',
'metadata' => '0',
'identifier_hash' => '475768e491580fb8b74ed36c2b1aaf619ca5e11d',
'folder_hash' => 'b4ab666a114d9905a50606d1837b74d952dfd90f',
],
],
'sys_file_metadata' => [
[
Expand Down Expand Up @@ -226,6 +247,14 @@
'cruser_id' => '1',
'file' => '5',
],
[
'uid' => '6',
'pid' => '0',
'tstamp' => '1371467047',
'crdate' => '1371467047',
'cruser_id' => '1',
'file' => '6',
],
],
'sys_file_reference' => [
[
Expand Down Expand Up @@ -324,6 +353,38 @@
'sorting_foreign' => '0',
'table_local' => 'sys_file',
],
[
'uid' => '7',
'pid' => '0',
'tstamp' => '1373537480',
'crdate' => '1371484347',
'cruser_id' => '1',
'deleted' => '0',
'hidden' => '0',
'sys_language_uid' => '0',
'uid_local' => '6',
'uid_foreign' => '0',
'tablenames' => 'pages',
'fieldname' => 'media',
'sorting_foreign' => '0',
'table_local' => 'sys_file',
],
[
'uid' => '8',
'pid' => '0',
'tstamp' => '1373537480',
'crdate' => '1371484347',
'cruser_id' => '1',
'deleted' => '0',
'hidden' => '0',
'sys_language_uid' => '0',
'uid_local' => '6',
'uid_foreign' => '1',
'tablenames' => 'tx_events_domain_model_event',
'fieldname' => 'images',
'sorting_foreign' => '2',
'table_local' => 'sys_file',
],
],
'tx_events_domain_model_region' => [
[
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions Tests/Functional/Cleanup/RemovePastTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,27 @@ public function removesPastData(): void
);

self::assertCount(
3,
4,
$this->getAllRecords('sys_file'),
'Unexpected number of sys_file records.'
);
self::assertCount(
3,
4,
$this->getAllRecords('sys_file_reference'),
'Unexpected number of sys_file_reference records.'
);
self::assertCount(
3,
4,
$this->getAllRecords('sys_file_metadata'),
'Unexpected number of sys_file_metadata records.'
);

$files = GeneralUtility::getFilesInDir('fileadmin/user_uploads');
self::assertIsArray($files, 'Failed to retrieve files from filesystem.');
self::assertCount(3, $files, 'Unexpected number of files in filesystem.');
self::assertSame('example-for-future-event.gif', array_values($files)[0], 'Unexpected file in filesystem.');
self::assertSame('example-for-partner.gif', array_values($files)[1], 'Unexpected file in filesystem.');
self::assertSame('example-to-keep.gif', array_values($files)[2], 'Unexpected file in filesystem.');
self::assertCount(4, $files, 'Unexpected number of files in filesystem.');
self::assertSame('example-events-image-used-somewhere-else.gif', array_values($files)[0], 'Unexpected file in filesystem.');
self::assertSame('example-for-future-event.gif', array_values($files)[1], 'Unexpected file in filesystem.');
self::assertSame('example-for-partner.gif', array_values($files)[2], 'Unexpected file in filesystem.');
self::assertSame('example-to-keep.gif', array_values($files)[3], 'Unexpected file in filesystem.');
}
}
25 changes: 25 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ parameters:
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Argument of an invalid type Doctrine\\\\DBAL\\\\Result\\|int supplied for foreach, only iterables are supported\\.$#"
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Argument of an invalid type Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int supplied for foreach, only iterables are supported\\.$#"
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Cannot call method fetchAllAssociativeIndexed\\(\\) on Doctrine\\\\DBAL\\\\Result\\|int\\.$#"
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Method Wrm\\\\Events\\\\Service\\\\Cleanup\\\\Files\\:\\:getPotentialFilesToDelete\\(\\) should return array\\<int, array\\{storage\\: int, identifier\\: string\\}\\> but returns array\\<int\\|string, array\\<string, mixed\\>\\>\\.$#"
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Method Wrm\\\\Events\\\\Service\\\\DestinationDataImportService\\\\CategoriesAssignment\\:\\:getCategories\\(\\) should return TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\ObjectStorage\\<Wrm\\\\Events\\\\Domain\\\\Model\\\\Category\\> but returns TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\ObjectStorage\\<mixed\\>\\.$#"
count: 2
Expand All @@ -65,6 +85,11 @@ parameters:
count: 1
path: Classes/Service/DestinationDataImportService/FilesAssignment.php

-
message: "#^Cannot call method fetchAllAssociativeIndexed\\(\\) on Doctrine\\\\DBAL\\\\Driver\\\\ResultStatement\\|int\\.$#"
count: 1
path: Classes/Service/Cleanup/Files.php

-
message: "#^Argument of an invalid type Doctrine\\\\DBAL\\\\Result\\|int supplied for foreach, only iterables are supported\\.$#"
count: 1
Expand Down