Skip to content

Commit

Permalink
[TASK] backport duplicatedFiles from TYPO3 14 without literal select
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrichmathes committed Oct 29, 2024
1 parent 86b1c2a commit 618f95e
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 135 deletions.
135 changes: 91 additions & 44 deletions Classes/Widgets/DuplicateFilesWidget.php
Original file line number Diff line number Diff line change
@@ -1,83 +1,130 @@
<?php

declare(strict_types=1);
/*
* 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!
*/
namespace Sitegeist\EditorWidgets\Widgets;

use Sitegeist\EditorWidgets\Traits\RequestAwareTrait;
use Sitegeist\EditorWidgets\Traits\WidgetTrait;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
use TYPO3\CMS\Dashboard\Widgets\RequestAwareWidgetInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;

final class DuplicateFilesWidget implements WidgetInterface, RequestAwareWidgetInterface, AdditionalCssInterface
{
use RequestAwareTrait;
use WidgetTrait;

/**
* @var array{showThumbnails: bool, thumbnailWidth: string, thumbnailHeight: string, duplicateLimit: int}
*/
private readonly array $options;
private ServerRequestInterface $request;
public function __construct(
private readonly BackendViewFactory $backendViewFactory,
private readonly ConnectionPool $connectionPool,
private readonly ResourceFactory $resourceFactory,
private readonly WidgetConfigurationInterface $configuration,
private readonly array $options = []
array $options = []
) {
$this->options = array_merge([
'showThumbnails' => true,
'thumbnailWidth' => '200m',
'thumbnailHeight' => '70m',
'duplicateLimit' => 200,
], $options);
}

public function renderWidgetContent(): string
{
$duplicates = $this->getDuplicates($this->getFileUidsFromSha1($this->getDuplicatedSha1()));
$view = $this->backendViewFactory->create($this->request, ['sitegeist/editor-widgets']);
$view->assignMultiple([
'duplicates' => $duplicates,
'options' => $this->options,
'configuration' => $this->configuration,
]);
return $view->render('DuplicateFilesWidget');
}
public function getDuplicatedSha1(): array
{
$queryBuilder = $this->connectionPool->getConnectionForTable('sys_file')->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();

$duplicates = $queryBuilder
->selectLiteral('GROUP_CONCAT(uid) as uids', 'sha1', 'count(*) as counting')
return $queryBuilder
->select('sha1')
->from('sys_file')
->where('missing = 0')
->andWhere('storage > 0')
->andWhere('name != "index.html"')
->andWhere('identifier NOT LIKE "%_recycler_%"')
->orderBy('counting', 'desc')
->addOrderBy('size', 'desc')
->groupBy('sha1')
->having('counting > 1')
->setMaxResults(200)
->where(
$queryBuilder->expr()->eq('missing', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
$queryBuilder->expr()->gt('storage', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
$queryBuilder->expr()->neq('name', $queryBuilder->createNamedParameter('index.html')),
$queryBuilder->expr()->notLike('identifier', $queryBuilder->quote('%_recycler_%')),
)
->groupBy('sha1', 'size')
->having('COUNT(*) > 1')
->setMaxResults((int)$this->options['duplicateLimit'])
->executeQuery()
->fetchAllAssociative();

foreach ($duplicates as &$duplicate) {
$duplicate['files'] = array_map(
->fetchFirstColumn();
}
public function getFileUidsFromSha1(array $duplicatedSha1): array
{
$queryBuilder = $this->connectionPool->getConnectionForTable('sys_file')->createQueryBuilder();
$uids = [];
foreach ($duplicatedSha1 as $sha1) {
$uids[] = $queryBuilder
->select('uid')
->from('sys_file')
->where(
$queryBuilder->expr()->eq('sha1', $queryBuilder->createNamedParameter($sha1, Connection::PARAM_STR))
)
->executeQuery()
->fetchFirstColumn();
}
return $uids;
}
private function getDuplicates(array $fileUidGroups): array
{
$duplicates = [];
foreach ($fileUidGroups as $fileUidList) {
$duplicates[] = array_filter(array_map(
function ($uid) {
$file = $this->resourceFactory->getFileObject($uid);

try {
$file = $this->resourceFactory->getFileObject((int)$uid);
if (!$file->exists() || $file->isMissing()) {
return null;
}
$file->getParentFolder();
} catch (\Throwable $th) {
$file->setMissing(1);
} catch (FileDoesNotExistException | InsufficientFolderAccessPermissionsException | \Exception) {
return null;
}

return [
'file' => $file,
'referenceCount' => BackendUtility::referenceCount('sys_file', $file->getUid()),
'isImage' => $file->isImage(),
];
},
GeneralUtility::trimExplode(',', $duplicate['uids'])
);
$fileUidList
));
}

$view = $this->backendViewFactory->create($this->request, ['sitegeist/editor-widgets']);
$view->assignMultiple([
'duplicates' => $duplicates,
'configuration' => $this->configuration,
]);

return $view->render('DuplicateFilesWidget');
return array_filter($duplicates, static function ($files) { return count($files) >= 2; });
}
public function getOptions(): array
{
return $this->options;
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}

public function getCssFiles(): array
{
return [
Expand Down
24 changes: 11 additions & 13 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,25 @@
<source>No unused files found</source>
</trans-unit>

<trans-unit id="widgets.duplicateFiles.title">
<trans-unit id="widgets.duplicateFiles.title" resname="widgets.duplicateFiles.title" xml:space="preserve">
<source>Duplicate files</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.description">
<source>Displays all files that are duplicates</source>
<trans-unit id="widgets.duplicateFiles.description" resname="widgets.duplicateFiles.description" xml:space="preserve">
<source>Displays files that exists multiple times</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.openOriginalFile">
<source>Open original file in new window</source>
<trans-unit id="widgets.duplicateFiles.empty" resname="widgets.duplicateFiles.empty" xml:space="preserve">
<source>No duplicate files found</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.control">
<trans-unit id="widgets.duplicateFiles.column.file" resname="widgets.duplicateFiles.column.file" xml:space="preserve">
<source>File</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.column.control" resname="widgets.duplicateFiles.column.control" xml:space="preserve">
<source>Control</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.referenceCount">
<trans-unit id="widgets.duplicateFiles.column.referenceCount" resname="widgets.duplicateFiles.column.referenceCount" xml:space="preserve">
<source>References</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.openFileList">
<source>Search in folder</source>
</trans-unit>
<trans-unit id="widgets.duplicateFiles.empty">
<source>No duplicates found</source>
</trans-unit>


<trans-unit id="widgets.lastChangedPages.title">
<source>Last changed pages</source>
Expand Down
135 changes: 57 additions & 78 deletions Resources/Private/Templates/DuplicateFilesWidget.html
Original file line number Diff line number Diff line change
@@ -1,100 +1,79 @@
<html
xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true"
>
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:core="http://typo3.org/ns/TYPO3/CMS/Core/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Widget" />
<f:section name="main">
<f:if condition="{duplicates}">
<f:then>
<div class="widget-table-wrapper">
<table class="widget-table">
<table class="table table-hover widget-table-group-striped">
<thead>
<tr>
<th width="2%"></th>
<th width="70%"></th>
<th width="15%"><f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.control" /></th>
<th width="10%"><f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.referenceCount" /></th>
<th class="col-title" colspan="2"><f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.column.file" /></th>
<th class="col-control nowrap"><span class="visually-hidden"><f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.column.control" /></span></th>
</tr>
</thead>
<f:for each="{duplicates}" as="duplicate">
<tr>
<td colspan="4">
<table style="width: 100%; margin:0; padding:0;">
<f:for each="{duplicate.files}" as="duplicate">
<tr style="background-color: initial;">
<td width="2%" style="padding:0;">
<core:iconForResource resource="{duplicate.file}" />
</td>
<td width="70%" style="word-break: break-word;">
{duplicate.file.identifier}<br />
<f:if condition="{duplicate.isImage}">
<f:media file="{duplicate.file}" width="150c" height="75c" />
</f:if>
</td>
<td width="15%">
<f:if condition="!{duplicate.file.missing}">
<f:then>
<div class="btn-group position-static">
<f:link.file
file="{duplicate.file}"
target="_blank"
title="{f:translate(key: 'LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.openOriginalFile')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-eye" />
</f:link.file>

<a
href="#"
data-dispatch-action="TYPO3.InfoWindow.showItem"
data-dispatch-args-list="_FILE,{duplicate.file.storage.storageRecord.uid}:{duplicate.file.identifier}"
title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-document-info" />
</a>

<f:be.link
route="media_management"
parameters="{id: duplicate.file.parentFolder.combinedIdentifier, searchTerm: duplicate.file.name}"
title="{f:translate(key: 'LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.openFileList')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-search" />
</f:be.link>
</div>
</f:then>
<f:else>
<span class="text-danger" title="{f:translate(key: 'LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:fileMissing.description')}">
<f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:fileMissing" />
</span>
</f:else>
</f:if>
</td>
<td width="10%" style="text-align: right; padding-right: 6px;">
{duplicate.referenceCount}
</td>
</tr>
</f:for>
</table>
</td>
</tr>
<f:for each="{duplicates}" as="duplicateGroup" iteration="groupIterator">
<f:for each="{duplicateGroup}" as="duplicate">
<tr class="{f:if(condition: groupIterator.isEven, then: 'group-odd', else: 'group-even')}">
<td class="col-thumbnail">
<f:if condition="{options.showThumbnails} && {duplicate.file.image}">
<div><f:media file="{duplicate.file}" width="{options.thumbnailWidth}" height="{options.thumbnailHeight}" /></div>
</f:if>
</td>
<td class="col-title col-word-break">
{duplicate.file.storage.storageRecord.name}: {duplicate.file.identifier} <br>
<f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.column.referenceCount" />: {duplicate.referenceCount}
</td>
<td class="col-control nowrap">
<div class="btn-group position-static">
<f:link.file
file="{duplicate.file}"
target="_blank"
title="{f:translate(key: 'LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:openOriginalFile')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-eye" />
</f:link.file>
<a
href="#"
data-dispatch-action="TYPO3.InfoWindow.showItem"
data-dispatch-args-list="_FILE,{duplicate.file.storage.storageRecord.uid}:{duplicate.file.identifier}"
title="{f:translate(key: 'LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-document-info" />
</a>
<f:be.link
route="media_management"
parameters="{id: duplicate.file.parentFolder.combinedIdentifier, searchTerm: duplicate.file.name}"
title="{f:translate(key: 'LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:openFileList')}"
class="btn btn-default btn-sm"
>
<core:icon identifier="actions-search" />
</f:be.link>
</div>
</td>
</tr>
</f:for>
</f:for>
</table>
</div>
</f:then>
<f:else>
<div class="sitegeist-editor-widgets_info-wrapper">
<p class="sitegeist-editor-widgets_empty-widget">
<core:icon identifier="actions-rocket" size="large" /><br />
<div class="callout callout-info">
<div class="callout-icon">
<span class="icon-emphasized">
<core:icon identifier="actions-approve" />
</span>
</div>
<div class="callout-content">
<f:translate key="LLL:EXT:editor_widgets/Resources/Private/Language/locallang.xlf:widgets.duplicateFiles.empty" />
</p>
</div>
</div>
</f:else>
</f:if>
</f:section>
<f:section name="footer">

</f:section>
</html>
2 changes: 2 additions & 0 deletions Resources/Public/Css/backend.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@
}
}
}

.widget-table-group-striped>tbody>tr.group-even>*{--typo3-table-bg-type:color-mix(in srgb, var(--typo3-table-bg), var(--typo3-table-color) 3%)}

0 comments on commit 618f95e

Please sign in to comment.