1// Copyright (C) 2012 David Faure <faure@kde.org>
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qsavefile.h"
5
6#if QT_CONFIG(temporaryfile)
7
8#include "qplatformdefs.h"
9#include "private/qsavefile_p.h"
10#include "qfileinfo.h"
11#include "qabstractfileengine_p.h"
12#include "qdebug.h"
13#include "qtemporaryfile.h"
14#include "private/qiodevice_p.h"
15#include "private/qtemporaryfile_p.h"
16#ifdef Q_OS_UNIX
17#include <errno.h>
18#endif
19
20QT_BEGIN_NAMESPACE
21
22using namespace Qt::StringLiterals;
23
24QSaveFilePrivate::QSaveFilePrivate()
25 : writeError(QFileDevice::NoError),
26 useTemporaryFile(true),
27 directWriteFallback(false)
28{
29}
30
31QSaveFilePrivate::~QSaveFilePrivate()
32{
33}
34
35/*!
36 \class QSaveFile
37 \inmodule QtCore
38 \brief The QSaveFile class provides an interface for safely writing to files.
39
40 \ingroup io
41
42 \reentrant
43
44 \since 5.1
45
46 QSaveFile is an I/O device for writing text and binary files, without losing
47 existing data if the writing operation fails.
48
49 While writing, the contents will be written to a temporary file, and if
50 no error happened, commit() will move it to the final file. This ensures that
51 no data at the final file is lost in case an error happens while writing,
52 and no partially-written file is ever present at the final location. Always
53 use QSaveFile when saving entire documents to disk.
54
55 QSaveFile automatically detects errors while writing, such as the full partition
56 situation, where write() cannot write all the bytes. It will remember that
57 an error happened, and will discard the temporary file in commit().
58
59 Much like with QFile, the file is opened with open(). Data is usually read
60 and written using QDataStream or QTextStream, but you can also directly call
61 \l write().
62
63 Unlike QFile, calling close() is not allowed. commit() replaces it. If commit()
64 was not called and the QSaveFile instance is destroyed, the temporary file is
65 discarded.
66
67 To abort saving due to an application error, call cancelWriting(), so that
68 even a call to commit() later on will not save.
69
70 \sa QTextStream, QDataStream, QFileInfo, QDir, QFile, QTemporaryFile
71*/
72
73#ifdef QT_NO_QOBJECT
74QSaveFile::QSaveFile(const QString &name)
75 : QFileDevice(*new QSaveFilePrivate)
76{
77 Q_D(QSaveFile);
78 d->fileName = name;
79}
80#else
81/*!
82 Constructs a new file object to represent the file with the given \a name.
83*/
84QSaveFile::QSaveFile(const QString &name)
85 : QFileDevice(*new QSaveFilePrivate, nullptr)
86{
87 Q_D(QSaveFile);
88 d->fileName = name;
89}
90
91/*!
92 Constructs a new file object with the given \a parent.
93 You need to call setFileName() before open().
94*/
95QSaveFile::QSaveFile(QObject *parent)
96 : QFileDevice(*new QSaveFilePrivate, parent)
97{
98}
99/*!
100 Constructs a new file object with the given \a parent to represent the
101 file with the specified \a name.
102*/
103QSaveFile::QSaveFile(const QString &name, QObject *parent)
104 : QFileDevice(*new QSaveFilePrivate, parent)
105{
106 Q_D(QSaveFile);
107 d->fileName = name;
108}
109#endif
110
111/*!
112 Destroys the file object, discarding the saved contents unless commit() was called.
113*/
114QSaveFile::~QSaveFile()
115{
116 Q_D(QSaveFile);
117 if (isOpen()) {
118 QFileDevice::close();
119 Q_ASSERT(d->fileEngine);
120 d->fileEngine->remove();
121 }
122}
123
124/*!
125 Returns the name set by setFileName() or to the QSaveFile
126 constructor.
127
128 \sa setFileName()
129*/
130QString QSaveFile::fileName() const
131{
132 return d_func()->fileName;
133}
134
135/*!
136 Sets the \a name of the file. The name can have no path, a
137 relative path, or an absolute path.
138
139 \sa QFile::setFileName(), fileName()
140*/
141void QSaveFile::setFileName(const QString &name)
142{
143 d_func()->fileName = name;
144}
145
146/*!
147 Opens the file using \a mode flags. Returns \c true if successful;
148 otherwise returns \c false.
149
150 Important: The flags for \a mode must include \l QIODeviceBase::WriteOnly. Other
151 common flags you can use are \l Text and \l Unbuffered. Flags not supported at the
152 moment are \l ReadOnly (and therefore \l ReadWrite), \l Append, \l NewOnly and \l ExistingOnly;
153 they will generate a runtime warning.
154
155 \sa setFileName(), QT_USE_NODISCARD_FILE_OPEN
156*/
157bool QSaveFile::open(OpenMode mode)
158{
159 Q_D(QSaveFile);
160 if (isOpen()) {
161 qWarning(msg: "QSaveFile::open: File (%ls) already open", qUtf16Printable(fileName()));
162 return false;
163 }
164 unsetError();
165 d->writeError = QFileDevice::NoError;
166 if ((mode & (ReadOnly | WriteOnly)) == 0) {
167 qWarning(msg: "QSaveFile::open: Open mode not specified");
168 return false;
169 }
170 // In the future we could implement ReadWrite by copying from the existing file to the temp file...
171 // The implications of NewOnly and ExistingOnly when used with QSaveFile need to be considered carefully...
172 if (mode & (ReadOnly | Append | NewOnly | ExistingOnly)) {
173 qWarning(msg: "QSaveFile::open: Unsupported open mode 0x%x", uint(mode.toInt()));
174 return false;
175 }
176
177 // check if existing file is writable
178 QFileInfo existingFile(d->fileName);
179 if (existingFile.exists() && !existingFile.isWritable()) {
180 d->setError(err: QFileDevice::WriteError, errorString: QSaveFile::tr(s: "Existing file %1 is not writable").arg(a: d->fileName));
181 d->writeError = QFileDevice::WriteError;
182 return false;
183 }
184
185 if (existingFile.isDir()) {
186 d->setError(err: QFileDevice::WriteError, errorString: QSaveFile::tr(s: "Filename refers to a directory"));
187 d->writeError = QFileDevice::WriteError;
188 return false;
189 }
190
191 // Resolve symlinks. Don't use QFileInfo::canonicalFilePath so it still give the expected
192 // target even if the file does not exist
193 d->finalFileName = d->fileName;
194 if (existingFile.isSymLink()) {
195 int maxDepth = 128;
196 while (--maxDepth && existingFile.isSymLink())
197 existingFile.setFile(existingFile.symLinkTarget());
198 if (maxDepth > 0)
199 d->finalFileName = existingFile.filePath();
200 }
201
202 auto openDirectly = [&]() {
203 d->fileEngine = QAbstractFileEngine::create(fileName: d->finalFileName);
204 if (d->fileEngine->open(openMode: mode | QIODevice::Unbuffered)) {
205 d->useTemporaryFile = false;
206 QFileDevice::open(mode);
207 return true;
208 }
209 return false;
210 };
211
212 bool requiresDirectWrite = false;
213#ifdef Q_OS_WIN
214 // check if it is an Alternate Data Stream
215 requiresDirectWrite = d->finalFileName == d->fileName && d->fileName.indexOf(u':', 2) > 1;
216#elif defined(Q_OS_ANDROID)
217 // check if it is a content:// URL
218 requiresDirectWrite = d->fileName.startsWith("content://"_L1);
219#endif
220 if (requiresDirectWrite) {
221 // yes, we can't rename onto it...
222 if (d->directWriteFallback) {
223 if (openDirectly())
224 return true;
225 d->setError(err: d->fileEngine->error(), errorString: d->fileEngine->errorString());
226 d->fileEngine.reset();
227 } else {
228 QString msg =
229 QSaveFile::tr(s: "QSaveFile cannot open '%1' without direct write fallback enabled.")
230 .arg(a: QDir::toNativeSeparators(pathName: d->fileName));
231 d->setError(err: QFileDevice::OpenError, errorString: msg);
232 }
233 return false;
234 }
235
236 d->fileEngine.reset(p: new QTemporaryFileEngine(&d->finalFileName, QTemporaryFileEngine::Win32NonShared));
237 // if the target file exists, we'll copy its permissions below,
238 // but until then, let's ensure the temporary file is not accessible
239 // to a third party
240 int perm = (existingFile.exists() ? 0600 : 0666);
241 static_cast<QTemporaryFileEngine *>(d->fileEngine.get())->initialize(file: d->finalFileName, mode: perm);
242 // Same as in QFile: QIODevice provides the buffering, so there's no need to request it from the file engine.
243 if (!d->fileEngine->open(openMode: mode | QIODevice::Unbuffered)) {
244 QFileDevice::FileError err = d->fileEngine->error();
245#ifdef Q_OS_UNIX
246 if (d->directWriteFallback && err == QFileDevice::OpenError && errno == EACCES) {
247 if (openDirectly())
248 return true;
249 err = d->fileEngine->error();
250 }
251#endif
252 if (err == QFileDevice::UnspecifiedError)
253 err = QFileDevice::OpenError;
254 d->setError(err, errorString: d->fileEngine->errorString());
255 d->fileEngine.reset();
256 return false;
257 }
258
259 d->useTemporaryFile = true;
260 QFileDevice::open(mode);
261 if (existingFile.exists())
262 setPermissions(existingFile.permissions());
263 return true;
264}
265
266/*!
267 \reimp
268 This method has been made private so that it cannot be called, in order to prevent mistakes.
269 In order to finish writing the file, call commit().
270 If instead you want to abort writing, call cancelWriting().
271*/
272void QSaveFile::close()
273{
274 qFatal(msg: "QSaveFile::close called");
275}
276
277/*!
278 Commits the changes to disk, if all previous writes were successful.
279
280 It is mandatory to call this at the end of the saving operation, otherwise the file will be
281 discarded.
282
283 If an error happened during writing, deletes the temporary file and returns \c false.
284 Otherwise, renames it to the final fileName and returns \c true on success.
285 Finally, closes the device.
286
287 \sa cancelWriting()
288*/
289bool QSaveFile::commit()
290{
291 Q_D(QSaveFile);
292 if (!d->fileEngine)
293 return false;
294
295 if (!isOpen()) {
296 qWarning(msg: "QSaveFile::commit: File (%ls) is not open", qUtf16Printable(fileName()));
297 return false;
298 }
299 QFileDevice::close(); // calls flush()
300
301 const auto &fe = d->fileEngine;
302
303 // Sync to disk if possible. Ignore errors (e.g. not supported).
304 fe->syncToDisk();
305
306 // ensure we act on either a close()/flush() failure or a previous write()
307 // problem
308 if (d->error == QFileDevice::NoError)
309 d->error = d->writeError;
310 d->writeError = QFileDevice::NoError;
311
312 if (d->useTemporaryFile) {
313 if (d->error != QFileDevice::NoError) {
314 fe->remove();
315 return false;
316 }
317 // atomically replace old file with new file
318 // Can't use QFile::rename for that, must use the file engine directly
319 Q_ASSERT(fe);
320 if (!fe->renameOverwrite(newName: d->finalFileName)) {
321 d->setError(err: fe->error(), errorString: fe->errorString());
322 fe->remove();
323 return false;
324 }
325 }
326
327 // return true if all previous write() calls succeeded and if close() and
328 // flush() succeeded.
329 return d->error == QFileDevice::NoError;
330}
331
332/*!
333 Cancels writing the new file.
334
335 If the application changes its mind while saving, it can call cancelWriting(),
336 which sets an error code so that commit() will discard the temporary file.
337
338 Alternatively, it can simply make sure not to call commit().
339
340 Further write operations are possible after calling this method, but none
341 of it will have any effect, the written file will be discarded.
342
343 This method has no effect when direct write fallback is used. This is the case
344 when saving over an existing file in a readonly directory: no temporary file can
345 be created, so the existing file is overwritten no matter what, and cancelWriting()
346 cannot do anything about that, the contents of the existing file will be lost.
347
348 \sa commit()
349*/
350void QSaveFile::cancelWriting()
351{
352 Q_D(QSaveFile);
353 if (!isOpen())
354 return;
355 d->setError(err: QFileDevice::WriteError, errorString: QSaveFile::tr(s: "Writing canceled by application"));
356 d->writeError = QFileDevice::WriteError;
357}
358
359/*!
360 \reimp
361*/
362qint64 QSaveFile::writeData(const char *data, qint64 len)
363{
364 Q_D(QSaveFile);
365 if (d->writeError != QFileDevice::NoError)
366 return -1;
367
368 const qint64 ret = QFileDevice::writeData(data, len);
369
370 if (d->error != QFileDevice::NoError)
371 d->writeError = d->error;
372 return ret;
373}
374
375/*!
376 Allows writing over the existing file if necessary.
377
378 QSaveFile creates a temporary file in the same directory as the final
379 file and atomically renames it. However this is not possible if the
380 directory permissions do not allow creating new files.
381 In order to preserve atomicity guarantees, open() fails when it
382 cannot create the temporary file.
383
384 In order to allow users to edit files with write permissions in a
385 directory with restricted permissions, call setDirectWriteFallback() with
386 \a enabled set to true, and the following calls to open() will fallback to
387 opening the existing file directly and writing into it, without the use of
388 a temporary file.
389 This does not have atomicity guarantees, i.e. an application crash or
390 for instance a power failure could lead to a partially-written file on disk.
391 It also means cancelWriting() has no effect, in such a case.
392
393 Typically, to save documents edited by the user, call setDirectWriteFallback(true),
394 and to save application internal files (configuration files, data files, ...), keep
395 the default setting which ensures atomicity.
396
397 \sa directWriteFallback()
398*/
399void QSaveFile::setDirectWriteFallback(bool enabled)
400{
401 Q_D(QSaveFile);
402 d->directWriteFallback = enabled;
403}
404
405/*!
406 Returns \c true if the fallback solution for saving files in read-only
407 directories is enabled.
408
409 \sa setDirectWriteFallback()
410*/
411bool QSaveFile::directWriteFallback() const
412{
413 Q_D(const QSaveFile);
414 return d->directWriteFallback;
415}
416
417QT_END_NAMESPACE
418
419#ifndef QT_NO_QOBJECT
420#include "moc_qsavefile.cpp"
421#endif
422
423#endif // QT_CONFIG(temporaryfile)
424

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtbase/src/corelib/io/qsavefile.cpp