Работа с файлами и директориями в Qt

Сравнение QFile, QSaveFile и атомарная запись данных

Введение

Представьте: приложение сохраняет важный конфигурационный файл, и в момент записи происходит сбой питания или принудительное завершение процесса. Если запись шла напрямую в целевой файл через QFile, результат — повреждённый, наполовину записанный файл, который потом не сможет прочитать ни одна программа. Для таких сценариев в Qt существует QSaveFile — класс, реализующий атомарную запись.

Концепция

QFile::write() пишет данные непосредственно в целевой файл. Если процесс прерывается на середине операции, файл остаётся в неконсистентном состоянии.

QSaveFile решает эту проблему иначе: данные сначала пишутся во временный файл рядом с целевым, и только при успешном вызове commit() происходит атомарное переименование временного файла в целевое имя (на уровне ОС это операция rename, которая либо выполняется полностью, либо не выполняется вовсе). Если что-то пошло не так — commit() просто не вызывается, и оригинальный файл остаётся нетронутым.

Пример кода: Сравнение QFile, QSaveFile

#include <QFile>
#include <QSaveFile>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>

bool saveConfigUnsafe(const QString &path, const QJsonObject &data)
{
    // Небезопасный вариант: при сбое посередине записи
    // файл может остаться повреждённым.
    QFile file(path);
    if (!file.open(QIODevice::WriteOnly))
        return false;

    QJsonDocument doc(data);
    qint64 written = file.write(doc.toJson());
    file.close();

    return written == doc.toJson().size();
}

bool saveConfigAtomic(const QString &path, const QJsonObject &data)
{
    QSaveFile saveFile(path);

    // Включаем прямую запись (без промежуточного буфера в памяти),
    // что важно для больших файлов.
    saveFile.setDirectWriteFallback(true);

    if (!saveFile.open(QIODevice::WriteOnly)) {
        qWarning() << "Не удалось открыть QSaveFile:" << saveFile.errorString();
        return false;
    }

    QJsonDocument doc(data);
    QByteArray json = doc.toJson();

    qint64 written = saveFile.write(json);
    if (written != json.size()) {
        qWarning() << "Ошибка записи данных";
        saveFile.cancelWriting(); // явно откатываем, временный файл удалится
        return false;
    }

    // Атомарно переименовывает временный файл в целевой.
    // Если commit() не вызван (или вернул false), оригинальный
    // файл по пути path остаётся неизменным.
    if (!saveFile.commit()) {
        qWarning() << "Не удалось завершить атомарную запись:" << saveFile.errorString();
        return false;
    }

    return true;
}

int main()
{
    QJsonObject config;
    config["version"] = 2;
    config["username"] = "qthelper_user";
    config["autosave"] = true;

    if (saveConfigAtomic("settings.json", config)) {
        qDebug() << "Конфигурация сохранена атомарно";
    }

    return 0;
}

Пояснения к коду

QSaveFile наследует интерфейс QIODevice, поэтому API для записи (write()) не отличается от обычного QFile. Главное отличие — в финальной фазе: вызов commit() обязателен, иначе изменения не применятся, а временный файл будет автоматически удалён при разрушении объекта.

setDirectWriteFallback(true) указывает, что если временный файл не может быть создан (например, нет прав на создание новых файлов в директории, но есть права на запись в существующий), QSaveFile попробует писать напрямую в целевой файл — жертвуя атомарностью ради работоспособности.

Подводные камни

  1. Забытый commit(). Самая частая ошибка — записать данные и забыть вызвать commit(). В этом случае все изменения будут отброшены, а исходный файл останется без изменений, что может маскировать ошибку (программа «молча» не сохраняет данные).
  1. Права на создание файлов в директории. Атомарное переименование требует, чтобы процесс имел права на создание нового файла в той же директории, где лежит целевой файл (временный файл создаётся рядом). Если у приложения есть только право записи в существующий файл, но не право создания новых — open() завершится ошибкой, если не включён setDirectWriteFallback().
  1. Атомарность ограничена одной файловой системой. Переименование атомарно только в пределах одного и того же тома/раздела. Если временный каталог находится на другом разделе — гарантии атомарности теряются.
  1. Сетевые файловые системы. На NFS, SMB и аналогичных сетевых ФС поведение rename может отличаться от локальных дисков, и строгая атомарность не всегда гарантируется производителем сервера.
  1. Производительность при частых небольших записях. Создание временного файла и rename для каждой мелкой операции дороже, чем прямая запись через QFile. QSaveFile оправдан для важных данных (конфигурации, документы пользователя), но избыточен для частых логов или кэшей.
  1. Дисковое пространство. Во время записи временно требуется место и для старого, и для нового файла одновременно — это нужно учитывать при работе с большими файлами на устройствах с ограниченным объёмом памяти.

Выбор между QFile и QSaveFile — это выбор между скоростью и надёжностью. Для пользовательских данных, конфигураций и документов атомарная запись через QSaveFile должна быть стандартом по умолчанию.

================================================================================