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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | using namespace Qt::StringLiterals; |
23 | |
24 | QSaveFilePrivate::QSaveFilePrivate() |
25 | : writeError(QFileDevice::NoError), |
26 | useTemporaryFile(true), |
27 | directWriteFallback(false) |
28 | { |
29 | } |
30 | |
31 | QSaveFilePrivate::~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 |
74 | QSaveFile::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 | */ |
84 | QSaveFile::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 | */ |
95 | QSaveFile::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 | */ |
103 | QSaveFile::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 | */ |
114 | QSaveFile::~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 | */ |
130 | QString 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 | */ |
141 | void 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 | */ |
157 | bool 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 | */ |
272 | void 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 | */ |
289 | bool 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 | */ |
350 | void 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 | */ |
362 | qint64 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 | */ |
399 | void 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 | */ |
411 | bool QSaveFile::directWriteFallback() const |
412 | { |
413 | Q_D(const QSaveFile); |
414 | return d->directWriteFallback; |
415 | } |
416 | |
417 | QT_END_NAMESPACE |
418 | |
419 | #ifndef QT_NO_QOBJECT |
420 | #include "moc_qsavefile.cpp" |
421 | #endif |
422 | |
423 | #endif // QT_CONFIG(temporaryfile) |
424 | |