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 | |
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 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 |
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 | */ |
94 | QSaveFile::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 | */ |
102 | QSaveFile::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 | */ |
113 | QSaveFile::~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 | */ |
129 | QString 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 | */ |
140 | void 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 | */ |
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.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 | */ |
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 = 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 | */ |
342 | void 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 | */ |
354 | qint64 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 | */ |
391 | void 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 | */ |
403 | bool QSaveFile::directWriteFallback() const |
404 | { |
405 | Q_D(const QSaveFile); |
406 | return d->directWriteFallback; |
407 | } |
408 | |
409 | QT_END_NAMESPACE |
410 | |
411 | #ifndef QT_NO_QOBJECT |
412 | #include "moc_qsavefile.cpp" |
413 | #endif |
414 | |
415 | #endif // QT_NO_TEMPORARYFILE |
416 | |