refs #545 CAtomicFile used in CValueCache to avoid corruption in case serialization is interrupted.

This commit is contained in:
Mathew Sutcliffe
2016-01-10 23:39:26 +00:00
parent 956d393bb1
commit f9de444a53
3 changed files with 162 additions and 7 deletions

View File

@@ -0,0 +1,91 @@
/* Copyright (C) 2015
* swift project Community / Contributors
*
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
* directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project,
* including this file, may be copied, modified, propagated, or distributed except according to the terms
* contained in the LICENSE file.
*/
#include "blackmisc/atomicfile.h"
#include "blackmisc/algorithm.h"
#include <QFileInfo>
#include <QDir>
#if defined(Q_OS_POSIX)
#include <stdio.h>
#elif defined(Q_OS_WIN32)
#include <windows.h>
#endif
namespace BlackMisc
{
bool CAtomicFile::open(CAtomicFile::OpenMode mode)
{
Q_ASSERT_X(!(mode & (ReadOnly | Append)), Q_FUNC_INFO, "ReadOnly and Append flags are incompatible with CAtomicFile");
m_originalFilename = fileName();
QFileInfo fileInfo(fileName());
setFileName(QFileInfo(fileInfo.dir(), ".tmp." + fileInfo.fileName() + "." + randomSuffix()).filePath());
if (exists()) { remove(); }
bool ok = QFile::open(mode);
if (! ok) { setFileName(m_originalFilename); }
return ok;
}
void CAtomicFile::close()
{
if (! isOpen()) { return; }
QFile::close();
if (error() == NoError) { replaceOriginal(); }
setFileName(m_originalFilename);
}
bool CAtomicFile::checkedClose()
{
close();
return error() == NoError;
}
CAtomicFile::FileError CAtomicFile::error() const
{
if (m_renameError) { return RenameError; }
return QFile::error();
}
void CAtomicFile::unsetError()
{
m_renameError = false;
QFile::unsetError();
}
QString CAtomicFile::randomSuffix()
{
Q_CONSTEXPR auto max = 2176782335;
return QString::number(std::uniform_int_distribution<std::decay<decltype(max)>::type>(0, max)(Private::defaultRandomGenerator()), 36);
}
#if defined(Q_OS_POSIX)
void CAtomicFile::replaceOriginal()
{
auto result = ::rename(qPrintable(fileName()), qPrintable(m_originalFilename));
if (result < 0) { m_renameError = true; }
}
#elif defined(Q_OS_WIN32)
void CAtomicFile::replaceOriginal()
{
auto result = MoveFileExA(qPrintable(fileName()), qPrintable(m_originalFilename), MOVEFILE_REPLACE_EXISTING);
if (! result) { m_renameError = true; }
}
#else
void CAtomicFile::replaceOriginal()
{
if (exists(m_originalFilename)) { remove(m_originalFilename); }
rename(m_originalFilename);
}
#endif
}

View File

@@ -0,0 +1,61 @@
/* Copyright (C) 2015
* swift project Community / Contributors
*
* This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level
* directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project,
* including this file, may be copied, modified, propagated, or distributed except according to the terms
* contained in the LICENSE file.
*/
//! \file
#ifndef BLACKMISC_ATOMICFILE_H
#define BLACKMISC_ATOMICFILE_H
#include <QFile>
#include "blackmisc/blackmiscexport.h"
namespace BlackMisc
{
/*!
* A subclass of QFile which writes to a temporary file while it is open, then renames the file
* when it is closed, so that it overwrites the target file as a single, atomic transaction.
*
* If the application crashes while data is still being written, the original file is unchanged.
*/
class BLACKMISC_EXPORT CAtomicFile : public QFile
{
public:
//! \copydoc QFile::QFile(const QString &)
CAtomicFile(const QString &filename) : QFile(filename) {}
//! \copydoc QFile::~QFile
~CAtomicFile() { close(); }
//! \copydoc QFile::open
//! Just before opening the file, the filename is changed so we actually write to a temporary file.
virtual bool open(OpenMode mode) override;
//! \copydoc QFileDevice::close
//! After closing the file, it is renamed so that it overwrites the target file.
virtual void close() override;
//! Calls close() and returns false if there was an error at any stage.
bool checkedClose();
//! \copydoc QFileDevice::error
FileError error() const;
//! \copydoc QFileDevice::unsetError
void unsetError();
private:
static QString randomSuffix();
void replaceOriginal();
QString m_originalFilename;
bool m_renameError = false;
};
}
#endif

View File

@@ -12,6 +12,7 @@
#include "blackmisc/logmessage.h"
#include "blackmisc/algorithm.h"
#include "blackmisc/lockfree.h"
#include "blackmisc/atomicfile.h"
#include <QThread>
#include <QJsonDocument>
@@ -230,23 +231,25 @@ namespace BlackMisc
}
for (auto it = namespaces.cbegin(); it != namespaces.cend(); ++it)
{
QFile file(dir + "/" + it.key() + ".json");
if (! file.open(QFile::ReadWrite | QFile::Text))
QFile readFile(dir + "/" + it.key() + ".json");
if (! readFile.open(QFile::ReadWrite | QFile::Text))
{
return CLogMessage(this).error("Failed to open %1: %2") << file.fileName() << file.errorString();
return CLogMessage(this).error("Failed to open %1: %2") << readFile.fileName() << readFile.errorString();
}
auto json = QJsonDocument::fromJson(file.readAll());
auto json = QJsonDocument::fromJson(readFile.readAll());
if (json.isArray() || (json.isNull() && ! json.isEmpty()))
{
return CLogMessage(this).error("Invalid JSON format in %1") << file.fileName();
return CLogMessage(this).error("Invalid JSON format in %1") << readFile.fileName();
}
CVariantMap storedValues;
storedValues.convertFromJson(json.object());
storedValues.insert(*it);
json.setObject(storedValues.toJson());
if (! (file.seek(0) && file.resize(0) && file.write(json.toJson()) > 0))
readFile.close();
CAtomicFile writeFile(readFile.fileName());
if (! (writeFile.open(QFile::WriteOnly | QFile::Text) && writeFile.write(json.toJson()) > 0 && writeFile.checkedClose()))
{
return CLogMessage(this).error("Failed to write to %1: %2") << file.fileName() << file.errorString();
return CLogMessage(this).error("Failed to write to %1: %2") << writeFile.fileName() << writeFile.errorString();
}
}
return {};