QSaveFile: allow saving to a writable file in a non-writable directory
The only way to make this possible is to disable the atomic-rename-from-temp-file behavior. This is not done by default, but only if the application allows this to happen. https://bugs.kde.org/show_bug.cgi?id=312415 Change-Id: I71ce54ae1f7f50ab5e8379f04c0ede74ebe3136d Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
parent
ef061b76b1
commit
b3a505dc92
@ -48,11 +48,16 @@
|
|||||||
#include "qtemporaryfile.h"
|
#include "qtemporaryfile.h"
|
||||||
#include "private/qiodevice_p.h"
|
#include "private/qiodevice_p.h"
|
||||||
#include "private/qtemporaryfile_p.h"
|
#include "private/qtemporaryfile_p.h"
|
||||||
|
#ifdef Q_OS_UNIX
|
||||||
|
#include <errno.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
|
|
||||||
QSaveFilePrivate::QSaveFilePrivate()
|
QSaveFilePrivate::QSaveFilePrivate()
|
||||||
: writeError(QFileDevice::NoError)
|
: writeError(QFileDevice::NoError),
|
||||||
|
useTemporaryFile(true),
|
||||||
|
directWriteFallback(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,6 +206,18 @@ bool QSaveFile::open(OpenMode mode)
|
|||||||
// Same as in QFile: QIODevice provides the buffering, so there's no need to request it from the file engine.
|
// Same as in QFile: QIODevice provides the buffering, so there's no need to request it from the file engine.
|
||||||
if (!d->fileEngine->open(mode | QIODevice::Unbuffered)) {
|
if (!d->fileEngine->open(mode | QIODevice::Unbuffered)) {
|
||||||
QFileDevice::FileError err = d->fileEngine->error();
|
QFileDevice::FileError err = d->fileEngine->error();
|
||||||
|
#ifdef Q_OS_UNIX
|
||||||
|
if (d->directWriteFallback && err == QFileDevice::OpenError && errno == EACCES) {
|
||||||
|
delete d->fileEngine;
|
||||||
|
d->fileEngine = QAbstractFileEngine::create(d->fileName);
|
||||||
|
if (d->fileEngine->open(mode | QIODevice::Unbuffered)) {
|
||||||
|
d->useTemporaryFile = false;
|
||||||
|
QFileDevice::open(mode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
err = d->fileEngine->error();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (err == QFileDevice::UnspecifiedError)
|
if (err == QFileDevice::UnspecifiedError)
|
||||||
err = QFileDevice::OpenError;
|
err = QFileDevice::OpenError;
|
||||||
d->setError(err, d->fileEngine->errorString());
|
d->setError(err, d->fileEngine->errorString());
|
||||||
@ -209,6 +226,7 @@ bool QSaveFile::open(OpenMode mode)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d->useTemporaryFile = true;
|
||||||
QFileDevice::open(mode);
|
QFileDevice::open(mode);
|
||||||
if (existingFile.exists())
|
if (existingFile.exists())
|
||||||
setPermissions(existingFile.permissions());
|
setPermissions(existingFile.permissions());
|
||||||
@ -253,6 +271,7 @@ bool QSaveFile::commit()
|
|||||||
// Sync to disk if possible. Ignore errors (e.g. not supported).
|
// Sync to disk if possible. Ignore errors (e.g. not supported).
|
||||||
d->fileEngine->syncToDisk();
|
d->fileEngine->syncToDisk();
|
||||||
|
|
||||||
|
if (d->useTemporaryFile) {
|
||||||
if (d->writeError != QFileDevice::NoError) {
|
if (d->writeError != QFileDevice::NoError) {
|
||||||
d->fileEngine->remove();
|
d->fileEngine->remove();
|
||||||
d->writeError = QFileDevice::NoError;
|
d->writeError = QFileDevice::NoError;
|
||||||
@ -270,6 +289,7 @@ bool QSaveFile::commit()
|
|||||||
d->fileEngine = 0;
|
d->fileEngine = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
delete d->fileEngine;
|
delete d->fileEngine;
|
||||||
d->fileEngine = 0;
|
d->fileEngine = 0;
|
||||||
return true;
|
return true;
|
||||||
@ -286,6 +306,11 @@ bool QSaveFile::commit()
|
|||||||
Further write operations are possible after calling this method, but none
|
Further write operations are possible after calling this method, but none
|
||||||
of it will have any effect, the written file will be discarded.
|
of it will have any effect, the written file will be discarded.
|
||||||
|
|
||||||
|
This method has no effect when direct write fallback is used. This is the case
|
||||||
|
when saving over an existing file in a readonly directory: no temporary file can
|
||||||
|
be created, so the existing file is overwritten no matter what, and cancelWriting()
|
||||||
|
cannot do anything about that, the contents of the existing file will be lost.
|
||||||
|
|
||||||
\sa commit()
|
\sa commit()
|
||||||
*/
|
*/
|
||||||
void QSaveFile::cancelWriting()
|
void QSaveFile::cancelWriting()
|
||||||
@ -313,4 +338,46 @@ qint64 QSaveFile::writeData(const char *data, qint64 len)
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Allows writing over the existing file if necessary.
|
||||||
|
|
||||||
|
QSaveFile creates a temporary file in the same directory as the final
|
||||||
|
file and atomically renames it. However this is not possible if the
|
||||||
|
directory permissions do not allow creating new files.
|
||||||
|
In order to preserve atomicity guarantees, open() fails when it
|
||||||
|
cannot create the temporary file.
|
||||||
|
|
||||||
|
In order to allow users to edit files with write permissions in a
|
||||||
|
directory with restricted permissions, call setDirectWriteFallback() with
|
||||||
|
\a enabled set to true, and the following calls to open() will fallback to
|
||||||
|
opening the existing file directly and writing into it, without the use of
|
||||||
|
a temporary file.
|
||||||
|
This does not have atomicity guarantees, i.e. an application crash or
|
||||||
|
for instance a power failure could lead to a partially-written file on disk.
|
||||||
|
It also means cancelWriting() has no effect, in such a case.
|
||||||
|
|
||||||
|
Typically, to save documents edited by the user, call setDirectWriteFallback(true),
|
||||||
|
and to save application internal files (configuration files, data files, ...), keep
|
||||||
|
the default setting which ensures atomicity.
|
||||||
|
|
||||||
|
\sa directWriteFallback()
|
||||||
|
*/
|
||||||
|
void QSaveFile::setDirectWriteFallback(bool enabled)
|
||||||
|
{
|
||||||
|
Q_D(QSaveFile);
|
||||||
|
d->directWriteFallback = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Returns true if the fallback solution for saving files in read-only
|
||||||
|
directories is enabled.
|
||||||
|
|
||||||
|
\sa setDirectWriteFallback()
|
||||||
|
*/
|
||||||
|
bool QSaveFile::directWriteFallback() const
|
||||||
|
{
|
||||||
|
Q_D(const QSaveFile);
|
||||||
|
return d->directWriteFallback;
|
||||||
|
}
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
@ -75,6 +75,9 @@ public:
|
|||||||
|
|
||||||
void cancelWriting();
|
void cancelWriting();
|
||||||
|
|
||||||
|
void setDirectWriteFallback(bool enabled);
|
||||||
|
bool directWriteFallback() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
qint64 writeData(const char *data, qint64 len) Q_DECL_OVERRIDE;
|
qint64 writeData(const char *data, qint64 len) Q_DECL_OVERRIDE;
|
||||||
|
|
||||||
|
@ -68,6 +68,9 @@ protected:
|
|||||||
QString fileName;
|
QString fileName;
|
||||||
|
|
||||||
QFileDevice::FileError writeError;
|
QFileDevice::FileError writeError;
|
||||||
|
|
||||||
|
bool useTemporaryFile;
|
||||||
|
bool directWriteFallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
QT_END_NAMESPACE
|
QT_END_NAMESPACE
|
||||||
|
@ -66,6 +66,7 @@ private slots:
|
|||||||
void textStreamManualFlush();
|
void textStreamManualFlush();
|
||||||
void textStreamAutoFlush();
|
void textStreamAutoFlush();
|
||||||
void saveTwice();
|
void saveTwice();
|
||||||
|
void transactionalWriteNoPermissionsOnDir_data();
|
||||||
void transactionalWriteNoPermissionsOnDir();
|
void transactionalWriteNoPermissionsOnDir();
|
||||||
void transactionalWriteNoPermissionsOnFile();
|
void transactionalWriteNoPermissionsOnFile();
|
||||||
void transactionalWriteCanceled();
|
void transactionalWriteCanceled();
|
||||||
@ -153,20 +154,86 @@ void tst_QSaveFile::textStreamAutoFlush()
|
|||||||
QFile::remove(targetFile);
|
QFile::remove(targetFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tst_QSaveFile::transactionalWriteNoPermissionsOnDir_data()
|
||||||
|
{
|
||||||
|
QTest::addColumn<bool>("directWriteFallback");
|
||||||
|
|
||||||
|
QTest::newRow("default") << false;
|
||||||
|
QTest::newRow("directWriteFallback") << true;
|
||||||
|
}
|
||||||
|
|
||||||
void tst_QSaveFile::transactionalWriteNoPermissionsOnDir()
|
void tst_QSaveFile::transactionalWriteNoPermissionsOnDir()
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_UNIX
|
#ifdef Q_OS_UNIX
|
||||||
if (::geteuid() == 0)
|
QFETCH(bool, directWriteFallback);
|
||||||
QSKIP("not valid running this test as root");
|
// Restore permissions so that the QTemporaryDir cleanup can happen
|
||||||
|
class PermissionRestorer
|
||||||
|
{
|
||||||
|
QString m_path;
|
||||||
|
public:
|
||||||
|
PermissionRestorer(const QString& path)
|
||||||
|
: m_path(path)
|
||||||
|
{}
|
||||||
|
|
||||||
// You can write into /dev/zero, but you can't create a /dev/zero.XXXXXX temp file.
|
~PermissionRestorer()
|
||||||
QSaveFile file("/dev/zero");
|
{
|
||||||
if (!QDir("/dev").exists())
|
restore();
|
||||||
QSKIP("/dev doesn't exist on this system");
|
}
|
||||||
|
void restore()
|
||||||
|
{
|
||||||
|
QFile file(m_path);
|
||||||
|
file.setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
QTemporaryDir dir;
|
||||||
|
QVERIFY(QFile(dir.path()).setPermissions(QFile::ReadOwner | QFile::ExeOwner));
|
||||||
|
PermissionRestorer permissionRestorer(dir.path());
|
||||||
|
|
||||||
|
const QString targetFile = dir.path() + QString::fromLatin1("/outfile");
|
||||||
|
QSaveFile firstTry(targetFile);
|
||||||
|
QVERIFY(!firstTry.open(QIODevice::WriteOnly));
|
||||||
|
QCOMPARE((int)firstTry.error(), (int)QFile::OpenError);
|
||||||
|
QVERIFY(!firstTry.commit());
|
||||||
|
|
||||||
|
// Now make an existing writable file
|
||||||
|
permissionRestorer.restore();
|
||||||
|
QFile f(targetFile);
|
||||||
|
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||||
|
QCOMPARE(f.write("Hello"), Q_INT64_C(5));
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
// Make the directory non-writable again
|
||||||
|
QVERIFY(QFile(dir.path()).setPermissions(QFile::ReadOwner | QFile::ExeOwner));
|
||||||
|
|
||||||
|
// And write to it again using QSaveFile; only works if directWriteFallback is enabled
|
||||||
|
QSaveFile file(targetFile);
|
||||||
|
file.setDirectWriteFallback(directWriteFallback);
|
||||||
|
QCOMPARE(file.directWriteFallback(), directWriteFallback);
|
||||||
|
if (directWriteFallback) {
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
QCOMPARE((int)file.error(), (int)QFile::NoError);
|
||||||
|
QCOMPARE(file.write("World"), Q_INT64_C(5));
|
||||||
|
QVERIFY(file.commit());
|
||||||
|
|
||||||
|
QFile reader(targetFile);
|
||||||
|
QVERIFY(reader.open(QIODevice::ReadOnly));
|
||||||
|
QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("World"));
|
||||||
|
reader.close();
|
||||||
|
|
||||||
|
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||||
|
QCOMPARE((int)file.error(), (int)QFile::NoError);
|
||||||
|
QCOMPARE(file.write("W"), Q_INT64_C(1));
|
||||||
|
file.cancelWriting(); // no effect, as per the documentation
|
||||||
|
QVERIFY(file.commit());
|
||||||
|
|
||||||
|
QVERIFY(reader.open(QIODevice::ReadOnly));
|
||||||
|
QCOMPARE(QString::fromLatin1(reader.readAll()), QString::fromLatin1("W"));
|
||||||
|
} else {
|
||||||
QVERIFY(!file.open(QIODevice::WriteOnly));
|
QVERIFY(!file.open(QIODevice::WriteOnly));
|
||||||
QCOMPARE((int)file.error(), (int)QFile::OpenError);
|
QCOMPARE((int)file.error(), (int)QFile::OpenError);
|
||||||
QVERIFY(!file.commit());
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user