sunnuntai 31. heinäkuuta 2016

Writing a file, with a bit more safety


When you're writing out, say, a current software state to file (be it save of a game, or status of a map program, or current audiobook locations), you may want to be sure that the old data is overwritten only if storing new data was successful. Your user might not be very happy that, say, location data is invalid and unfixable (due to failed map settings store), or that all locations of audiobooks and podcasts are forgotten (unfortunately latter isn't my software so I can't but request author to fix it, but alas, no activity for ages...).

Like the link above may tell you, this concerns mostly Sailfish OS and thus Qt toolkit, but same principles apply for other systems too. Some systems however make this very, very difficult as actual implementation is very carefully hidden from you and may or may not be robust enough to do this for you.

Previously, due to simple laziness, I stored settings somewhat like this:

    QString file = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/config";
    QFile f(file);
    f.open(QIODevice::WriteOnly);
    QDataStream stream(&f);
    stream.setVersion(QDataStream::Qt_5_0);

    stream << data;
    // ...
    stream << lastData;

No close; since this is C++ this will be done automatically when function goes out of scope.

However, this will fail horribly if there is a problem, like running out of disk space or something else going wrong, and you may end up with zero-length file. And surprise surprise, something did go wrong today and I lost my map halfway through 30-mile biking trek, in middle of area I hadn't visited before. Just frickin' great.

What exactly happened with phone, I don't know. Some apps started behaving very weirdly. Eventually I restarted the phone which fixed most of the issue, but corrupted settings caused a bit more grief.

But this was sufficient motivation to fix the issue, hopefully once and for all.

To fix this, the change is relatively tiny:

    QString file = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/config.tmp";
    // ...
    stream << lastData;

    bool stastusOk = false;
    if (stream.status() == QDataStream::Ok) {
        statusOk = true;
    }

    f.close();

    if (statusOk) {
        file = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/config";
        QFile f2(file);
        if (f2.exists())
            f2.remove();

        f.rename(file);
    }

So, initially we create a temporary file where data is stored; once we know that the data was written successfully, that file is moved to replace the original file. Unfortunately Qt does not have function to "rename and delete target if it exists" so we have to add that extra part with f2 there. If rename then fails, this does all kinds of nasty things, but that, hopefully, will not happen. Of course you can check also the .tmp file on load if you are that paranoid (or just write alternatingly to two or three files, checking newest on load); I trust above to be sufficient. Until proven wrong, of course.

The documentation is not exactly clear whether stream status is available after closing the file, or if closing is needed, so I err'd on the side of caution as I didn't feel like experimenting.

Ignoring the details, the priciple is same for other systems too. First write temporary data, then copy over to replace original. This way the changes of data loss and corruption are minimized. If you want to get really paranoid a data checksum can also be thrown in for good measure.



Ei kommentteja:

Lähetä kommentti