| 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 |  |