Add QFile::moveToTrash, which moves a file to the trash

Due to the nature of QFile just operating on a file path, this also
works for paths that are actually directories.

The test covers files from different locations on which this
operation should typically succeed, but tries to handle the case
where trashing files will fail because of the file system
structure.

On Windows 7, running the test will open a confirmation dialog as
the implementation of IFileOperation doesn't respect the various
flags. This might depend on the specific Windows 7 patch level,
and the option to always use SHFileOperation on that platform needs
to be evaluated further.

[ChangeLog][QtCore][QFile] Introduce QFile::moveToTrash to allow
applications to move files to the trash.

Change-Id: I45019040c25b30f7db293b6933c63aca2f319514
Fixes: QTBUG-47703
Reviewed-by: Vitaly Fanaskov <vitaly.fanaskov@qt.io>
bb10
Volker Hilsheimer 2020-01-21 15:51:31 +01:00
parent 7dd6d32657
commit 74a2467edd
3 changed files with 203 additions and 0 deletions

View File

@ -551,6 +551,63 @@ QFile::remove(const QString &fileName)
return QFile(fileName).remove();
}
/*!
Moves the file specified by fileName() to the trash. Returns \c true if successful,
and sets the fileName() to the path at which the file can be found within the trash;
otherwise returns \c false.
\note On systems where the system API doesn't report the location of the file in the
trash, fileName() will be set to the null string once the file has been moved. On
systems that don't have a trash can, this function always returns false.
*/
bool
QFile::moveToTrash()
{
Q_D(QFile);
if (d->fileName.isEmpty() &&
!static_cast<QFSFileEngine *>(d->engine())->isUnnamedFile()) {
qWarning("QFile::remove: Empty or null file name");
return false;
}
unsetError();
close();
if (error() == QFile::NoError) {
QFileSystemEntry fileEntry(d->fileName);
QFileSystemEntry trashEntry;
QSystemError error;
if (QFileSystemEngine::moveFileToTrash(fileEntry, trashEntry, error)) {
setFileName(trashEntry.filePath());
unsetError();
return true;
}
d->setError(QFile::RenameError, error.toString());
}
return false;
}
/*!
\overload
Moves the file specified by fileName() to the trash. Returns \c true if successful,
and sets \a pathInTrash (if provided) to the path at which the file can be found within
the trash; otherwise returns \c false.
\note On systems where the system API doesn't report the path of the file in the
trash, \a pathInTrash will be set to the null string once the file has been moved.
On systems that don't have a trash can, this function always returns false.
*/
bool
QFile::moveToTrash(const QString &fileName, QString *pathInTrash)
{
QFile file(fileName);
if (file.moveToTrash()) {
if (pathInTrash)
*pathInTrash = file.fileName();
return true;
}
return false;
}
/*!
Renames the file currently specified by fileName() to \a newName.
Returns \c true if successful; otherwise returns \c false.

View File

@ -125,6 +125,9 @@ public:
bool remove();
static bool remove(const QString &fileName);
bool moveToTrash();
static bool moveToTrash(const QString &fileName, QString *pathInTrash = nullptr);
bool rename(const QString &newName);
static bool rename(const QString &oldName, const QString &newName);

View File

@ -279,6 +279,9 @@ private slots:
void reuseQFile();
void moveToTrash_data();
void moveToTrash();
private:
#ifdef BUILTIN_TESTDATA
QSharedPointer<QTemporaryDir> m_dataDir;
@ -3672,5 +3675,145 @@ void tst_QFile::reuseQFile()
}
}
void tst_QFile::moveToTrash_data()
{
QTest::addColumn<QString>("source");
QTest::addColumn<bool>("create");
QTest::addColumn<bool>("result");
if (QOperatingSystemVersion::current() <= QOperatingSystemVersion::Windows7)
QSKIP("Windows 7 insists on showing a confirmation dialog", SkipAll);
// success cases
{
QTemporaryFile temp;
QVERIFY(temp.open());
QTest::newRow("temporary file") << temp.fileName() << true << true;
}
{
QTemporaryDir tempDir;
tempDir.setAutoRemove(false);
QTest::newRow("temporary dir")
<< tempDir.path() + QLatin1Char('/')
<< true << true;
}
{
QTemporaryDir homeDir(QDir::homePath() + QLatin1String("/XXXXXX"));
homeDir.setAutoRemove(false);
QTemporaryFile homeFile(homeDir.path()
+ QLatin1String("/tst_qfile-XXXXXX"));
homeFile.open();
QTest::newRow("home file")
<< homeFile.fileName()
<< true << true;
QTest::newRow("home dir")
<< homeDir.path() + QLatin1Char('/')
<< true << true;
}
QTest::newRow("relative") << QStringLiteral("tst_qfile_moveToTrash.tmp") << true << true;
// failure cases
QTest::newRow("root") << QDir::rootPath() << false << false;
QTest::newRow("no-such-file") << QString::fromLatin1("no/such/file") << false << false;
}
void tst_QFile::moveToTrash()
{
QFETCH(QString, source);
QFETCH(bool, create);
QFETCH(bool, result);
/* This test makes assumptions about the file system layout
which might be wrong - moveToTrash may fail if the file lives
on a file system that is different from the home file system, and
has no .Trash directory.
*/
const bool mayFail = QStorageInfo(source) != QStorageInfo(QDir::home());
#if defined(Q_OS_WINRT)
QSKIP("WinRT does not have a trash", SkipAll);
#endif
auto ensureFile = [](const QString &source, bool create) {
if (QFileInfo::exists(source) || !create)
return;
if (source.endsWith(QLatin1Char('/'))) {
QDir::root().mkdir(source);
QFile file(source + QLatin1String("test"));
if (!file.open(QIODevice::WriteOnly))
QSKIP("Couldn't create directory with file");
} else {
QFile sourceFile(source);
QVERIFY2(sourceFile.open(QFile::WriteOnly | QFile::Text), qPrintable(sourceFile.errorString()));
sourceFile.close();
}
};
auto cleanupFile = [source, create]() {
if (!QFileInfo::exists(source) || !create)
return;
if (source.endsWith(QLatin1Char('/'))) {
QDir(source).removeRecursively();
} else {
QFile sourceFile(source);
sourceFile.remove();
}
};
// non-static version
{
ensureFile(source, create);
QFile sourceFile(source);
const bool success = sourceFile.moveToTrash();
// tolerate moveToTrash failing
if (result && !success && mayFail)
result = false;
if (result) {
// if any of the test fails, we still want to remove the file
auto onFailure = qScopeGuard(cleanupFile);
QVERIFY2(success, qPrintable(sourceFile.errorString()));
QCOMPARE(sourceFile.error(), QFile::NoError);
QVERIFY(source != sourceFile.fileName());
if (!sourceFile.fileName().isEmpty()) {
QVERIFY2(sourceFile.exists(), qPrintable(sourceFile.fileName()));
// remove file/dir in trash as well, don't fill disk
if (source.endsWith(QLatin1Char('/')))
QDir(sourceFile.fileName()).removeRecursively();
else
sourceFile.remove();
}
} else {
QVERIFY(!success);
QVERIFY(!sourceFile.errorString().isEmpty());
QCOMPARE(source, sourceFile.fileName());
}
}
// don't retry
if (mayFail)
return;
// static version
{
ensureFile(source, create);
QString pathInTrash;
const bool success = QFile::moveToTrash(source, &pathInTrash);
QCOMPARE(success, result);
if (result) {
auto onFailure = qScopeGuard(cleanupFile);
QVERIFY(source != pathInTrash);
if (!pathInTrash.isEmpty()) {
// remove file/dir in trash as well, don't fill disk
QVERIFY2(QFile::exists(pathInTrash), qPrintable(pathInTrash));
if (source.endsWith(QLatin1Char('/')))
QDir(pathInTrash).removeRecursively();
else
QFile::remove(pathInTrash);
}
}
}
}
QTEST_MAIN(tst_QFile)
#include "tst_qfile.moc"