2022-11-30 12:34:49 +03:00
/*
2023-01-25 00:31:09 +03:00
* Copyright 2021 ( c ) Felix Weilbach < felix . weilbach @ nextcloud . com >
* Copyright 2022 ( c ) Matthieu Gallien < matthieu . gallien @ nextcloud . com >
* Copyright 2022 ( c ) Claudio Cambra < claudio . cambra @ nextcloud . com >
2022-11-30 12:34:49 +03:00
*
* This program is free software ; you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation ; either version 2 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful , but
* WITHOUT ANY WARRANTY ; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE . See the GNU General Public License
* for more details .
*/
# include "caseclashfilenamedialog.h"
# include "ui_caseclashfilenamedialog.h"
# include "account.h"
# include "folder.h"
# include <QPushButton>
# include <QDir>
# include <QAbstractButton>
# include <QDialogButtonBox>
# include <QFileInfo>
# include <QPushButton>
# include <QDirIterator>
# include <QDesktopServices>
# include <QLoggingCategory>
# include <array>
namespace {
constexpr std : : array < QChar , 9 > caseClashIllegalCharacters ( { ' \\ ' , ' / ' , ' : ' , ' ? ' , ' * ' , ' \" ' , ' < ' , ' > ' , ' | ' } ) ;
QVector < QChar > getCaseClashIllegalCharsFromString ( const QString & string )
{
QVector < QChar > result ;
for ( const auto & character : string ) {
if ( std : : find ( caseClashIllegalCharacters . begin ( ) , caseClashIllegalCharacters . end ( ) , character )
! = caseClashIllegalCharacters . end ( ) ) {
result . push_back ( character ) ;
}
}
return result ;
}
QString caseClashIllegalCharacterListToString ( const QVector < QChar > & illegalCharacters )
{
QString illegalCharactersString ;
if ( illegalCharacters . size ( ) > 0 ) {
illegalCharactersString + = illegalCharacters [ 0 ] ;
}
for ( int i = 1 ; i < illegalCharacters . count ( ) ; + + i ) {
if ( illegalCharactersString . contains ( illegalCharacters [ i ] ) ) {
continue ;
}
illegalCharactersString + = " " + illegalCharacters [ i ] ;
}
return illegalCharactersString ;
}
}
namespace OCC {
Q_LOGGING_CATEGORY ( lcCaseClashConflictFialog , " nextcloud.sync.caseclash.dialog " , QtInfoMsg )
CaseClashFilenameDialog : : CaseClashFilenameDialog ( AccountPtr account ,
Folder * folder ,
const QString & conflictFilePath ,
const QString & conflictTaggedPath ,
QWidget * parent )
: QDialog ( parent )
, _ui ( std : : make_unique < Ui : : CaseClashFilenameDialog > ( ) )
, _conflictSolver ( conflictFilePath , conflictTaggedPath , folder - > remotePath ( ) , folder - > path ( ) , account , folder - > journalDb ( ) )
, _account ( account )
, _folder ( folder )
2022-12-14 22:17:24 +03:00
, _filePath ( std : : move ( conflictFilePath ) )
2022-11-30 12:34:49 +03:00
{
Q_ASSERT ( _account ) ;
Q_ASSERT ( _folder ) ;
const auto filePathFileInfo = QFileInfo ( _filePath ) ;
2022-12-14 22:17:24 +03:00
const auto conflictFileName = filePathFileInfo . fileName ( ) ;
2022-11-30 12:34:49 +03:00
_relativeFilePath = filePathFileInfo . path ( ) + QStringLiteral ( " / " ) ;
_relativeFilePath = _relativeFilePath . replace ( folder - > path ( ) , QLatin1String ( ) ) ;
2024-02-17 23:04:27 +03:00
_relativeFilePath = _relativeFilePath . isEmpty ( ) ? QString ( ) : _relativeFilePath ;
if ( ! _relativeFilePath . isEmpty ( ) & & ! _relativeFilePath . endsWith ( QStringLiteral ( " / " ) ) ) {
_relativeFilePath + = QStringLiteral ( " / " ) ;
}
2022-11-30 12:34:49 +03:00
2022-12-14 22:17:24 +03:00
_originalFileName = _relativeFilePath + conflictFileName ;
2022-11-30 12:34:49 +03:00
_ui - > setupUi ( this ) ;
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok ) - > setEnabled ( false ) ;
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok ) - > setText ( tr ( " Rename file " ) ) ;
_ui - > descriptionLabel - > setText ( tr ( " The file \" %1 \" could not be synced because of a case clash conflict with an existing file on this system. " ) . arg ( _originalFileName ) ) ;
2022-12-05 16:28:33 +03:00
_ui - > explanationLabel - > setText ( tr ( " %1 does not support equal file names with only letter casing differences. " ) . arg ( QSysInfo : : prettyProductName ( ) ) ) ;
2022-12-14 22:17:24 +03:00
_ui - > filenameLineEdit - > setText ( conflictFileName ) ;
const auto preexistingConflictingFile = caseClashConflictFile ( _filePath ) ;
updateFileWidgetGroup ( preexistingConflictingFile ,
2022-12-14 22:25:07 +03:00
tr ( " Open existing file " ) ,
2022-12-14 22:17:24 +03:00
_ui - > localVersionFilename ,
_ui - > localVersionLink ,
_ui - > localVersionMtime ,
_ui - > localVersionSize ,
_ui - > localVersionButton ) ;
updateFileWidgetGroup ( conflictTaggedPath ,
2022-12-14 22:25:07 +03:00
tr ( " Open clashing file " ) ,
2022-12-14 22:17:24 +03:00
_ui - > remoteVersionFilename ,
_ui - > remoteVersionLink ,
_ui - > remoteVersionMtime ,
_ui - > remoteVersionSize ,
_ui - > remoteVersionButton ) ;
// Display incoming conflict filename, not conflict-tagged filename
_ui - > remoteVersionFilename - > setText ( filePathFileInfo . fileName ( ) ) ;
2022-12-05 16:52:07 +03:00
adjustSize ( ) ;
2022-11-30 12:34:49 +03:00
connect ( _ui - > buttonBox , & QDialogButtonBox : : accepted , this , & QDialog : : accept ) ;
connect ( _ui - > buttonBox , & QDialogButtonBox : : rejected , this , & QDialog : : reject ) ;
2022-12-14 22:17:24 +03:00
connect ( _ui - > localVersionButton , & QToolButton : : clicked , this , [ preexistingConflictingFile ] {
QDesktopServices : : openUrl ( QUrl : : fromLocalFile ( preexistingConflictingFile ) ) ;
} ) ;
connect ( _ui - > remoteVersionButton , & QToolButton : : clicked , this , [ conflictTaggedPath ] {
QDesktopServices : : openUrl ( QUrl : : fromLocalFile ( conflictTaggedPath ) ) ;
} ) ;
2022-11-30 12:34:49 +03:00
_ui - > errorLabel - > setText ( { } /*
tr ( " Checking rename permissions … " ) */ ) ;
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok ) - > setEnabled ( false ) ;
_ui - > filenameLineEdit - > setEnabled ( false ) ;
connect ( _ui - > filenameLineEdit , & QLineEdit : : textChanged , this ,
& CaseClashFilenameDialog : : onFilenameLineEditTextChanged ) ;
connect ( & _conflictSolver , & CaseClashConflictSolver : : errorStringChanged , this , [ this ] ( ) {
_ui - > errorLabel - > setText ( _conflictSolver . errorString ( ) ) ;
} ) ;
connect ( & _conflictSolver , & CaseClashConflictSolver : : allowedToRenameChanged , this , [ this ] ( ) {
_ui - > buttonBox - > setStandardButtons ( _ui - > buttonBox - > standardButtons ( ) & ~ QDialogButtonBox : : No ) ;
if ( _conflictSolver . allowedToRename ( ) ) {
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok ) - > setEnabled ( true ) ;
_ui - > filenameLineEdit - > setEnabled ( true ) ;
_ui - > filenameLineEdit - > selectAll ( ) ;
} else {
_ui - > buttonBox - > setStandardButtons ( _ui - > buttonBox - > standardButtons ( ) | QDialogButtonBox : : No ) ;
}
} ) ;
connect ( & _conflictSolver , & CaseClashConflictSolver : : failed , this , [ this ] ( ) {
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok ) - > setEnabled ( false ) ;
} ) ;
connect ( & _conflictSolver , & CaseClashConflictSolver : : done , this , [ this ] ( ) {
Q_EMIT successfulRename ( _folder - > remotePath ( ) + _newFilename ) ;
QDialog : : accept ( ) ;
} ) ;
checkIfAllowedToRename ( ) ;
}
CaseClashFilenameDialog : : ~ CaseClashFilenameDialog ( ) = default ;
QString CaseClashFilenameDialog : : caseClashConflictFile ( const QString & conflictFilePath )
{
const auto filePathFileInfo = QFileInfo ( conflictFilePath ) ;
const auto conflictFileName = filePathFileInfo . fileName ( ) ;
QDirIterator it ( filePathFileInfo . path ( ) , QDirIterator : : Subdirectories ) ;
while ( it . hasNext ( ) ) {
const auto filePath = it . next ( ) ;
qCDebug ( lcCaseClashConflictFialog ) < < filePath ;
QFileInfo fileInfo ( filePath ) ;
if ( fileInfo . isDir ( ) ) {
continue ;
}
const auto currentFileName = fileInfo . fileName ( ) ;
if ( currentFileName . compare ( conflictFileName , Qt : : CaseInsensitive ) = = 0 & &
currentFileName ! = conflictFileName ) {
return filePath ;
}
}
return { } ;
}
void CaseClashFilenameDialog : : updateFileWidgetGroup ( const QString & filePath ,
const QString & linkText ,
QLabel * filenameLabel ,
QLabel * linkLabel ,
QLabel * mtimeLabel ,
QLabel * sizeLabel ,
QToolButton * button ) const
{
const auto filePathFileInfo = QFileInfo ( filePath ) ;
const auto filename = filePathFileInfo . fileName ( ) ;
const auto lastModifiedString = filePathFileInfo . lastModified ( ) . toString ( ) ;
const auto fileSizeString = locale ( ) . formattedDataSize ( filePathFileInfo . size ( ) ) ;
const auto fileUrl = QUrl : : fromLocalFile ( filePath ) . toString ( ) ;
const auto linkString = QStringLiteral ( " <a href='%1'>%2</a> " ) . arg ( fileUrl , linkText ) ;
2024-02-17 23:04:27 +03:00
const auto mime = QMimeDatabase ( ) . mimeTypeForFile ( _filePath , QMimeDatabase : : MatchExtension ) ;
2022-11-30 12:34:49 +03:00
QIcon fileTypeIcon ;
qCDebug ( lcCaseClashConflictFialog ) < < filePath < < filePathFileInfo . exists ( ) < < filename < < lastModifiedString < < fileSizeString < < fileUrl < < linkString < < mime ;
if ( QIcon : : hasThemeIcon ( mime . iconName ( ) ) ) {
fileTypeIcon = QIcon : : fromTheme ( mime . iconName ( ) ) ;
} else {
fileTypeIcon = QIcon ( " :/qt-project.org/styles/commonstyle/images/file-128.png " ) ;
}
filenameLabel - > setText ( filename ) ;
mtimeLabel - > setText ( lastModifiedString ) ;
sizeLabel - > setText ( fileSizeString ) ;
linkLabel - > setText ( linkString ) ;
button - > setIcon ( fileTypeIcon ) ;
}
void CaseClashFilenameDialog : : checkIfAllowedToRename ( )
{
_conflictSolver . checkIfAllowedToRename ( ) ;
}
bool CaseClashFilenameDialog : : processLeadingOrTrailingSpacesError ( const QString & fileName )
{
const auto hasLeadingSpaces = fileName . startsWith ( QLatin1Char ( ' ' ) ) ;
const auto hasTrailingSpaces = fileName . endsWith ( QLatin1Char ( ' ' ) ) ;
_ui - > buttonBox - > setStandardButtons ( _ui - > buttonBox - > standardButtons ( ) & ~ QDialogButtonBox : : No ) ;
if ( hasLeadingSpaces | | hasTrailingSpaces ) {
if ( hasLeadingSpaces & & hasTrailingSpaces ) {
_ui - > errorLabel - > setText ( tr ( " Filename contains leading and trailing spaces. " ) ) ;
}
else if ( hasLeadingSpaces ) {
_ui - > errorLabel - > setText ( tr ( " Filename contains leading spaces. " ) ) ;
} else if ( hasTrailingSpaces ) {
_ui - > errorLabel - > setText ( tr ( " Filename contains trailing spaces. " ) ) ;
}
if ( ! Utility : : isWindows ( ) ) {
_ui - > buttonBox - > setStandardButtons ( _ui - > buttonBox - > standardButtons ( ) | QDialogButtonBox : : No ) ;
_ui - > buttonBox - > button ( QDialogButtonBox : : No ) - > setText ( tr ( " Use invalid name " ) ) ;
}
return true ;
}
return false ;
}
void CaseClashFilenameDialog : : accept ( )
{
_newFilename = _relativeFilePath + _ui - > filenameLineEdit - > text ( ) . trimmed ( ) ;
_conflictSolver . solveConflict ( _newFilename ) ;
}
void CaseClashFilenameDialog : : onFilenameLineEditTextChanged ( const QString & text )
{
const auto isNewFileNameDifferent = text ! = _originalFileName ;
const auto illegalContainedCharacters = getCaseClashIllegalCharsFromString ( text ) ;
const auto containsIllegalChars = ! illegalContainedCharacters . empty ( ) | | text . endsWith ( QLatin1Char ( ' . ' ) ) ;
const auto isTextValid = isNewFileNameDifferent & & ! containsIllegalChars ;
_ui - > errorLabel - > setText ( " " ) ;
if ( ! processLeadingOrTrailingSpacesError ( text ) & & ! isTextValid ) {
_ui - > errorLabel - > setText ( tr ( " Filename contains illegal characters: %1 " ) . arg ( caseClashIllegalCharacterListToString ( illegalContainedCharacters ) ) ) ;
}
_ui - > buttonBox - > button ( QDialogButtonBox : : Ok )
- > setEnabled ( isTextValid ) ;
}
}