|
|
@ -43,39 +43,13 @@ |
|
|
|
|
|
|
|
|
|
|
|
using namespace std::chrono_literals; |
|
|
|
using namespace std::chrono_literals; |
|
|
|
|
|
|
|
|
|
|
|
namespace |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
// Encapsulates serialization of settings in "atomic" way.
|
|
|
|
|
|
|
|
// write() does not leave half-written files,
|
|
|
|
|
|
|
|
// read() has a workaround for a case of power loss during a previous serialization
|
|
|
|
|
|
|
|
class TransactionalSettings |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
public: |
|
|
|
|
|
|
|
explicit TransactionalSettings(const QString &name) |
|
|
|
|
|
|
|
: m_name(name) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QVariantHash read() const; |
|
|
|
|
|
|
|
bool write(const QVariantHash &data) const; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
|
|
// we return actual file names used by QSettings because
|
|
|
|
|
|
|
|
// there is no other way to get that name except
|
|
|
|
|
|
|
|
// actually create a QSettings object.
|
|
|
|
|
|
|
|
// if serialization operation was not successful we return empty string
|
|
|
|
|
|
|
|
Path deserialize(const QString &name, QVariantHash &data) const; |
|
|
|
|
|
|
|
Path serialize(const QString &name, const QVariantHash &data) const; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const QString m_name; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SettingsStorage *SettingsStorage::m_instance = nullptr; |
|
|
|
SettingsStorage *SettingsStorage::m_instance = nullptr; |
|
|
|
|
|
|
|
|
|
|
|
SettingsStorage::SettingsStorage() |
|
|
|
SettingsStorage::SettingsStorage() |
|
|
|
: m_data {TransactionalSettings(u"qBittorrent"_qs).read()} |
|
|
|
: m_nativeSettingsName {u"qBittorrent"_qs} |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
readNativeSettings(); |
|
|
|
|
|
|
|
|
|
|
|
m_timer.setSingleShot(true); |
|
|
|
m_timer.setSingleShot(true); |
|
|
|
m_timer.setInterval(5s); |
|
|
|
m_timer.setInterval(5s); |
|
|
|
connect(&m_timer, &QTimer::timeout, this, &SettingsStorage::save); |
|
|
|
connect(&m_timer, &QTimer::timeout, this, &SettingsStorage::save); |
|
|
@ -108,8 +82,7 @@ bool SettingsStorage::save() |
|
|
|
const QWriteLocker locker(&m_lock); // guard for `m_dirty` too
|
|
|
|
const QWriteLocker locker(&m_lock); // guard for `m_dirty` too
|
|
|
|
if (!m_dirty) return true; |
|
|
|
if (!m_dirty) return true; |
|
|
|
|
|
|
|
|
|
|
|
const TransactionalSettings settings(u"qBittorrent"_qs); |
|
|
|
if (!writeNativeSettings()) |
|
|
|
if (!settings.write(m_data)) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
m_timer.start(); |
|
|
|
m_timer.start(); |
|
|
|
return false; |
|
|
|
return false; |
|
|
@ -137,40 +110,40 @@ void SettingsStorage::storeValueImpl(const QString &key, const QVariant &value) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void SettingsStorage::removeValue(const QString &key) |
|
|
|
void SettingsStorage::readNativeSettings() |
|
|
|
{ |
|
|
|
{ |
|
|
|
const QWriteLocker locker(&m_lock); |
|
|
|
// We return actual file names used by QSettings because
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) |
|
|
|
// there is no other way to get that name except actually create a QSettings object.
|
|
|
|
if (m_data.remove(key)) |
|
|
|
// If serialization operation was not successful we return empty string.
|
|
|
|
#else |
|
|
|
const auto deserialize = [](QVariantHash &data, const QString &nativeSettingsName) -> Path |
|
|
|
if (m_data.remove(key) > 0) |
|
|
|
|
|
|
|
#endif |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
m_dirty = true; |
|
|
|
std::unique_ptr<QSettings> nativeSettings = Profile::instance()->applicationSettings(nativeSettingsName); |
|
|
|
m_timer.start(); |
|
|
|
if (nativeSettings->allKeys().isEmpty()) |
|
|
|
} |
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool SettingsStorage::hasKey(const QString &key) const |
|
|
|
// Copy everything into memory. This means even keys inserted in the file manually
|
|
|
|
{ |
|
|
|
// or that we don't touch directly in this code (eg disabled by ifdef). This ensures
|
|
|
|
const QReadLocker locker {&m_lock}; |
|
|
|
// that they will be copied over when save our settings to disk.
|
|
|
|
return m_data.contains(key); |
|
|
|
for (const QString &key : asConst(nativeSettings->allKeys())) |
|
|
|
} |
|
|
|
{ |
|
|
|
|
|
|
|
const QVariant value = nativeSettings->value(key); |
|
|
|
|
|
|
|
if (value.isValid()) |
|
|
|
|
|
|
|
data[key] = value; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
QVariantHash TransactionalSettings::read() const |
|
|
|
return Path(nativeSettings->fileName()); |
|
|
|
{ |
|
|
|
}; |
|
|
|
QVariantHash res; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Path newPath = deserialize(m_name + u"_new", res); |
|
|
|
const Path newPath = deserialize(m_data, (m_nativeSettingsName + u"_new")); |
|
|
|
if (!newPath.isEmpty()) |
|
|
|
if (!newPath.isEmpty()) |
|
|
|
{ // "_new" file is NOT empty
|
|
|
|
{ |
|
|
|
|
|
|
|
// "_new" file is NOT empty
|
|
|
|
// This means that the PC closed either due to power outage
|
|
|
|
// This means that the PC closed either due to power outage
|
|
|
|
// or because the disk was full. In any case the settings weren't transferred
|
|
|
|
// or because the disk was full. In any case the settings weren't transferred
|
|
|
|
// in their final position. So assume that qbittorrent_new.ini/qbittorrent_new.conf
|
|
|
|
// in their final position. So assume that qbittorrent_new.ini/qbittorrent_new.conf
|
|
|
|
// contains the most recent settings.
|
|
|
|
// contains the most recent settings.
|
|
|
|
LogMsg(QObject::tr("Detected unclean program exit. Using fallback file to restore settings: %1") |
|
|
|
LogMsg(tr("Detected unclean program exit. Using fallback file to restore settings: %1") |
|
|
|
.arg(newPath.toString()) |
|
|
|
.arg(newPath.toString()), Log::WARNING); |
|
|
|
, Log::WARNING); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QString finalPathStr = newPath.data(); |
|
|
|
QString finalPathStr = newPath.data(); |
|
|
|
const int index = finalPathStr.lastIndexOf(u"_new", -1, Qt::CaseInsensitive); |
|
|
|
const int index = finalPathStr.lastIndexOf(u"_new", -1, Qt::CaseInsensitive); |
|
|
@ -182,26 +155,43 @@ QVariantHash TransactionalSettings::read() const |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
else |
|
|
|
{ |
|
|
|
{ |
|
|
|
deserialize(m_name, res); |
|
|
|
deserialize(m_data, m_nativeSettingsName); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return res; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
bool TransactionalSettings::write(const QVariantHash &data) const |
|
|
|
bool SettingsStorage::writeNativeSettings() const |
|
|
|
{ |
|
|
|
{ |
|
|
|
|
|
|
|
std::unique_ptr<QSettings> nativeSettings = Profile::instance()->applicationSettings(m_nativeSettingsName + u"_new"); |
|
|
|
|
|
|
|
|
|
|
|
// QSettings deletes the file before writing it out. This can result in problems
|
|
|
|
// QSettings deletes the file before writing it out. This can result in problems
|
|
|
|
// if the disk is full or a power outage occurs. Those events might occur
|
|
|
|
// if the disk is full or a power outage occurs. Those events might occur
|
|
|
|
// between deleting the file and recreating it. This is a safety measure.
|
|
|
|
// between deleting the file and recreating it. This is a safety measure.
|
|
|
|
// Write everything to qBittorrent_new.ini/qBittorrent_new.conf and if it succeeds
|
|
|
|
// Write everything to qBittorrent_new.ini/qBittorrent_new.conf and if it succeeds
|
|
|
|
// replace qBittorrent.ini/qBittorrent.conf with it.
|
|
|
|
// replace qBittorrent.ini/qBittorrent.conf with it.
|
|
|
|
const Path newPath = serialize(m_name + u"_new", data); |
|
|
|
for (auto i = m_data.begin(); i != m_data.end(); ++i) |
|
|
|
if (newPath.isEmpty()) |
|
|
|
nativeSettings->setValue(i.key(), i.value()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nativeSettings->sync(); // Important to get error status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (nativeSettings->status()) |
|
|
|
{ |
|
|
|
{ |
|
|
|
Utils::Fs::removeFile(newPath); |
|
|
|
case QSettings::NoError: |
|
|
|
return false; |
|
|
|
break; |
|
|
|
|
|
|
|
case QSettings::AccessError: |
|
|
|
|
|
|
|
LogMsg(tr("An access error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case QSettings::FormatError: |
|
|
|
|
|
|
|
LogMsg(tr("A format error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
LogMsg(tr("An unknown error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nativeSettings->status() != QSettings::NoError) |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Path newPath {nativeSettings->fileName()}; |
|
|
|
QString finalPathStr = newPath.data(); |
|
|
|
QString finalPathStr = newPath.data(); |
|
|
|
const int index = finalPathStr.lastIndexOf(u"_new", -1, Qt::CaseInsensitive); |
|
|
|
const int index = finalPathStr.lastIndexOf(u"_new", -1, Qt::CaseInsensitive); |
|
|
|
finalPathStr.remove(index, 4); |
|
|
|
finalPathStr.remove(index, 4); |
|
|
@ -211,47 +201,22 @@ bool TransactionalSettings::write(const QVariantHash &data) const |
|
|
|
return Utils::Fs::renameFile(newPath, finalPath); |
|
|
|
return Utils::Fs::renameFile(newPath, finalPath); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Path TransactionalSettings::deserialize(const QString &name, QVariantHash &data) const |
|
|
|
void SettingsStorage::removeValue(const QString &key) |
|
|
|
{ |
|
|
|
{ |
|
|
|
SettingsPtr settings = Profile::instance()->applicationSettings(name); |
|
|
|
const QWriteLocker locker(&m_lock); |
|
|
|
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) |
|
|
|
if (settings->allKeys().isEmpty()) |
|
|
|
if (m_data.remove(key)) |
|
|
|
return {}; |
|
|
|
#else |
|
|
|
|
|
|
|
if (m_data.remove(key) > 0) |
|
|
|
// Copy everything into memory. This means even keys inserted in the file manually
|
|
|
|
#endif |
|
|
|
// or that we don't touch directly in this code (eg disabled by ifdef). This ensures
|
|
|
|
|
|
|
|
// that they will be copied over when save our settings to disk.
|
|
|
|
|
|
|
|
for (const QString &key : asConst(settings->allKeys())) |
|
|
|
|
|
|
|
{ |
|
|
|
{ |
|
|
|
const QVariant value = settings->value(key); |
|
|
|
m_dirty = true; |
|
|
|
if (value.isValid()) |
|
|
|
m_timer.start(); |
|
|
|
data[key] = value; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return Path(settings->fileName()); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Path TransactionalSettings::serialize(const QString &name, const QVariantHash &data) const |
|
|
|
bool SettingsStorage::hasKey(const QString &key) const |
|
|
|
{ |
|
|
|
{ |
|
|
|
SettingsPtr settings = Profile::instance()->applicationSettings(name); |
|
|
|
const QReadLocker locker {&m_lock}; |
|
|
|
for (auto i = data.begin(); i != data.end(); ++i) |
|
|
|
return m_data.contains(key); |
|
|
|
settings->setValue(i.key(), i.value()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
settings->sync(); // Important to get error status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (settings->status()) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
case QSettings::NoError: |
|
|
|
|
|
|
|
return Path(settings->fileName()); |
|
|
|
|
|
|
|
case QSettings::AccessError: |
|
|
|
|
|
|
|
LogMsg(QObject::tr("An access error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case QSettings::FormatError: |
|
|
|
|
|
|
|
LogMsg(QObject::tr("A format error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
LogMsg(QObject::tr("An unknown error occurred while trying to write the configuration file."), Log::CRITICAL); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return {}; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|