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#ifndef QT_NO_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 call the
61 QIODevice-inherited functions read(), readLine(), readAll(), 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*/
94QSaveFile::QSaveFile(QObject *parent)
95 : QFileDevice(*new QSaveFilePrivate, parent)
96{
97}
98/*!
99 Constructs a new file object with the given \a parent to represent the
100 file with the specified \a name.
101*/
102QSaveFile::QSaveFile(const QString &name, QObject *parent)
103 : QFileDevice(*new QSaveFilePrivate, parent)
104{
105 Q_D(QSaveFile);
106 d->fileName = name;
107}
108#endif
109
110/*!
111 Destroys the file object, discarding the saved contents unless commit() was called.
112*/
113QSaveFile::~QSaveFile()
114{
115 Q_D(QSaveFile);
116 QFileDevice::close();
117 if (d->fileEngine) {
118 d->fileEngine->remove();
119 d->fileEngine.reset();
120 }
121}
122
123/*!
124 Returns the name set by setFileName() or to the QSaveFile
125 constructor.
126
127 \sa setFileName()
128*/
129QString QSaveFile::fileName() const
130{
131 return d_func()->fileName;
132}
133
134/*!
135 Sets the \a name of the file. The name can have no path, a
136 relative path, or an absolute path.
137
138 \sa QFile::setFileName(), fileName()
139*/
140void QSaveFile::setFileName(const QString &name)
141{
142 d_func()->fileName = name;
143}
144
145/*!
146 Opens the file using OpenMode \a mode, returning true if successful;
147 otherwise false.
148
149 Important: the \a mode must include QIODevice::WriteOnly.
150 It may also have additional flags, such as QIODevice::Text and QIODevice::Unbuffered.
151
152 QIODevice::ReadWrite, QIODevice::Append, QIODevice::NewOnly and
153 QIODevice::ExistingOnly are not supported at the moment.
154
155 \sa QIODevice::OpenMode, setFileName()
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.reset(p: 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 = std::move(d->fileEngine);
302
303 // Sync to disk if possible. Ignore errors (e.g. not supported).
304 fe->syncToDisk();
305
306 if (d->useTemporaryFile) {
307 if (d->writeError != QFileDevice::NoError) {
308 fe->remove();
309 d->writeError = QFileDevice::NoError;
310 return false;
311 }
312 // atomically replace old file with new file
313 // Can't use QFile::rename for that, must use the file engine directly
314 Q_ASSERT(fe);
315 if (!fe->renameOverwrite(newName: d->finalFileName)) {
316 d->setError(err: fe->error(), errorString: fe->errorString());
317 fe->remove();
318 return false;
319 }
320 }
321 return true;
322}
323
324/*!
325 Cancels writing the new file.
326
327 If the application changes its mind while saving, it can call cancelWriting(),
328 which sets an error code so that commit() will discard the temporary file.
329
330 Alternatively, it can simply make sure not to call commit().
331
332 Further write operations are possible after calling this method, but none
333 of it will have any effect, the written file will be discarded.
334
335 This method has no effect when direct write fallback is used. This is the case
336 when saving over an existing file in a readonly directory: no temporary file can
337 be created, so the existing file is overwritten no matter what, and cancelWriting()
338 cannot do anything about that, the contents of the existing file will be lost.
339
340 \sa commit()
341*/
342void QSaveFile::cancelWriting()
343{
344 Q_D(QSaveFile);
345 if (!isOpen())
346 return;
347 d->setError(err: QFileDevice::WriteError, errorString: QSaveFile::tr(s: "Writing canceled by application"));
348 d->writeError = QFileDevice::WriteError;
349}
350
351/*!
352 \reimp
353*/
354qint64 QSaveFile::writeData(const char *data, qint64 len)
355{
356 Q_D(QSaveFile);
357 if (d->writeError != QFileDevice::NoError)
358 return -1;
359
360 const qint64 ret = QFileDevice::writeData(data, len);
361
362 if (d->error != QFileDevice::NoError)
363 d->writeError = d->error;
364 return ret;
365}
366
367/*!
368 Allows writing over the existing file if necessary.
369
370 QSaveFile creates a temporary file in the same directory as the final
371 file and atomically renames it. However this is not possible if the
372 directory permissions do not allow creating new files.
373 In order to preserve atomicity guarantees, open() fails when it
374 cannot create the temporary file.
375
376 In order to allow users to edit files with write permissions in a
377 directory with restricted permissions, call setDirectWriteFallback() with
378 \a enabled set to true, and the following calls to open() will fallback to
379 opening the existing file directly and writing into it, without the use of
380 a temporary file.
381 This does not have atomicity guarantees, i.e. an application crash or
382 for instance a power failure could lead to a partially-written file on disk.
383 It also means cancelWriting() has no effect, in such a case.
384
385 Typically, to save documents edited by the user, call setDirectWriteFallback(true),
386 and to save application internal files (configuration files, data files, ...), keep
387 the default setting which ensures atomicity.
388
389 \sa directWriteFallback()
390*/
391void QSaveFile::setDirectWriteFallback(bool enabled)
392{
393 Q_D(QSaveFile);
394 d->directWriteFallback = enabled;
395}
396
397/*!
398 Returns \c true if the fallback solution for saving files in read-only
399 directories is enabled.
400
401 \sa setDirectWriteFallback()
402*/
403bool QSaveFile::directWriteFallback() const
404{
405 Q_D(const QSaveFile);
406 return d->directWriteFallback;
407}
408
409QT_END_NAMESPACE
410
411#ifndef QT_NO_QOBJECT
412#include "moc_qsavefile.cpp"
413#endif
414
415#endif // QT_NO_TEMPORARYFILE
416

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