1
0
mirror of https://github.com/nomic-ai/gpt4all.git synced 2025-05-06 15:37:19 +00:00
This commit is contained in:
Jared Van Bortel 2025-03-19 18:08:49 -04:00
parent 9772027e5e
commit b359c9245c
11 changed files with 250 additions and 162 deletions

View File

@ -79,25 +79,6 @@ ColumnLayout {
})[provider.id.toString()]
}
}
RemoteModelCard {
width: parent.childWidth
height: parent.childHeight
providerUsesApiKey: false
providerName: qsTr("Ollama (Custom)")
providerImage: "qrc:/gpt4all/icons/antenna_3.svg"
providerDesc: qsTr("Configure a custom Ollama provider.")
}
// TODO(jared): add custom openai back to the list
/*
RemoteModelCard {
width: parent.childWidth
height: parent.childHeight
providerIsCustom: true
providerName: qsTr("Custom")
providerImage: "qrc:/gpt4all/icons/antenna_3.svg"
providerDesc: qsTr("The custom provider option allows users to connect their own OpenAI-compatible AI models or third-party inference services. This is useful for organizations with proprietary models or those leveraging niche AI providers not listed here.")
}
*/
}
}
}

View File

@ -4,17 +4,12 @@ import QtQuick.Layouts
Rectangle {
id: remoteModelCard
property var provider: null
property var provider // required
property alias providerName: providerNameLabel.text
property alias providerImage: myimage.source
property alias providerDesc: providerDescLabel.text
property bool providerUsesApiKey: true
// for internal use
property bool apiKeyRequired: provider === null ? providerUsesApiKey : "apiKey" in provider
property bool apiKeyGood: !apiKeyRequired // (overwritten later if required)
property bool baseUrlGood: provider !== null // (overwritten later if custom)
color: theme.conversationBackground
radius: 10
border.width: 1
@ -78,7 +73,38 @@ Rectangle {
spacing: 30
ColumnLayout {
visible: apiKeyRequired
visible: !provider.isBuiltin
MySettingsLabel {
text: qsTr("Name")
font.bold: true
font.pixelSize: theme.fontSizeLarge
color: theme.settingsTitleTextColor
}
MyTextField {
id: nameField
property bool initialized: false
property bool ok: true
Layout.fillWidth: true
font.pixelSize: theme.fontSizeLarge
wrapMode: Text.WrapAnywhere
Component.onCompleted: {
text = provider.name;
initialized = true;
}
onTextChanged: {
if (!initialized) return;
ok = provider.setNameQml(text.trim());
}
placeholderText: qsTr("Provider Name")
Accessible.role: Accessible.EditableText
Accessible.name: placeholderText
}
}
ColumnLayout {
visible: "apiKey" in provider
MySettingsLabel {
text: qsTr("API Key")
@ -89,27 +115,32 @@ Rectangle {
MyTextField {
id: apiKeyField
property bool initialized: false
property bool ok: false
Layout.fillWidth: true
font.pixelSize: theme.fontSizeLarge
wrapMode: Text.WrapAnywhere
echoMode: TextField.Password
function showError() {
messageToast.show(qsTr("ERROR: $API_KEY is empty."));
apiKeyField.placeholderTextColor = theme.textErrorColor;
Component.onCompleted: {
if (parent.visible) {
text = provider.apiKey;
ok = text.trim() != "";
} else
ok = true;
initialized = true;
}
Component.onCompleted: { if (parent.visible && provider !== null) { text = provider.apiKey; } }
onTextChanged: {
apiKeyField.placeholderTextColor = theme.mutedTextColor;
if (provider !== null) { apiKeyGood = provider.setApiKeyQml(text) && text !== ""; }
if (!initialized) return;
ok = provider.setApiKeyQml(text.trim()) && text.trim() !== "";
}
placeholderText: qsTr("enter $API_KEY")
placeholderText: qsTr("Provider API Key")
Accessible.role: Accessible.EditableText
Accessible.name: placeholderText
}
}
ColumnLayout {
visible: provider === null
visible: !provider.isBuiltin
MySettingsLabel {
text: qsTr("Base Url")
font.bold: true
@ -118,18 +149,20 @@ Rectangle {
}
MyTextField {
id: baseUrlField
property bool initialized: false
property bool ok: true
Layout.fillWidth: true
font.pixelSize: theme.fontSizeLarge
wrapMode: Text.WrapAnywhere
function showError() {
messageToast.show(qsTr("ERROR: $BASE_URL is empty."));
baseUrlField.placeholderTextColor = theme.textErrorColor;
Component.onCompleted: {
text = provider.baseUrl;
initialized = true;
}
onTextChanged: {
baseUrlField.placeholderTextColor = theme.mutedTextColor;
baseUrlGood = text.trim() !== "";
if (!initialized) return;
ok = provider.setBaseUrlQml(text.trim()) && text.trim() !== "";
}
placeholderText: qsTr("enter $BASE_URL")
placeholderText: qsTr("Provider Base URL")
Accessible.role: Accessible.EditableText
Accessible.name: placeholderText
}
@ -152,25 +185,15 @@ Rectangle {
Layout.fillWidth: true
id: myModelList
currentIndex: -1
property bool ready: baseUrlGood && apiKeyGood
property bool ready: nameField.ok && baseUrlField.ok && apiKeyField.ok
onReadyChanged: {
if (!ready) { return; }
let providerRef = null; // owns the new provider
let provider = remoteModelCard.provider;
if (provider === null) {
// TODO: custom OpenAI
providerRef = QmlFunctions.newCustomOllamaProvider("foo", baseUrlField.text.trim());
if (providerRef !== null)
provider = providerRef.get();
}
if (provider !== null) {
provider.listModelsQml().then(modelList => {
if (modelList !== null) {
model = modelList;
currentIndex = -1;
}
});
}
if (!ready) return;
provider.listModelsQml().then(modelList => {
if (modelList !== null) {
model = modelList;
currentIndex = -1;
}
});
}
}
}
@ -183,7 +206,6 @@ Rectangle {
font.pixelSize: theme.fontSizeLarge
property string apiKeyText: apiKeyField.text.trim()
property string baseUrlText: provider === null ? baseUrlField.text.trim() : provider.baseUrl
property string modelNameText: myModelList.currentText.trim()
enabled: baseUrlGood && apiKeyGood && modelNameText !== ""

View File

@ -46,7 +46,8 @@ protected:
class OllamaProvider : public QObject, public virtual ModelProvider {
Q_OBJECT
Q_PROPERTY(QUuid id READ id CONSTANT)
Q_PROPERTY(QUuid id READ id CONSTANT)
Q_PROPERTY(bool isBuiltin READ isBuiltin CONSTANT)
protected:
explicit OllamaProvider();

View File

@ -98,14 +98,6 @@ OpenaiProvider::OpenaiProvider(QString apiKey)
OpenaiProvider::~OpenaiProvider() noexcept = default;
Q_INVOKABLE bool OpenaiProvider::setApiKeyQml(QString value)
{
auto res = setApiKey(std::move(value));
if (!res)
qWarning().noquote() << "setApiKey failed:" << res.error().errorString();
return bool(res);
}
auto OpenaiProvider::supportedGenerationParams() const -> QSet<GenerationParam>
{
using enum GenerationParam;

View File

@ -50,8 +50,9 @@ protected:
class OpenaiProvider : public QObject, public virtual ModelProvider {
Q_OBJECT
Q_PROPERTY(QUuid id READ id CONSTANT )
Q_PROPERTY(QString apiKey READ apiKey NOTIFY apiKeyChanged)
Q_PROPERTY(QUuid id READ id CONSTANT )
Q_PROPERTY(QString apiKey READ apiKey NOTIFY apiKeyChanged)
Q_PROPERTY(bool isBuiltin READ isBuiltin CONSTANT )
protected:
explicit OpenaiProvider();
@ -66,7 +67,8 @@ public:
[[nodiscard]] const QString &apiKey() const { return m_apiKey; }
[[nodiscard]] virtual DataStoreResult<> setApiKey(QString value) = 0;
Q_INVOKABLE bool setApiKeyQml(QString value);
Q_INVOKABLE bool setApiKeyQml(QString value)
{ return wrapQmlFunc(this, &OpenaiProvider::setApiKey, u"setApiKey", std::move(value)); }
auto supportedGenerationParams() const -> QSet<GenerationParam> override;
auto makeGenerationParams(const QMap<GenerationParam, QVariant> &values) const -> OpenaiGenerationParams * override;
@ -107,7 +109,7 @@ public:
std::unordered_set<QString> modelWhitelist);
[[nodiscard]] DataStoreResult<> setApiKey(QString value) override
{ return setMemberProp<QString>(&OpenaiProviderBuiltin::m_apiKey, "apiKey", std::move(value), /*createName*/ m_name); }
{ return setMemberProp<QString>(&OpenaiProviderBuiltin::m_apiKey, "apiKey", std::move(value), /*create*/ true); }
// override for model whitelist
auto listModels() -> QCoro::Task<backend::DataOrRespErr<QStringList>> override;

View File

@ -82,6 +82,13 @@ ModelProviderMutable::~ModelProviderMutable() noexcept
res.error().raise(); // should not happen - will terminate program
}
DataStoreResult<> ModelProviderCustom::setName(QString value)
{
if (value.isEmpty())
return std::unexpected(u"name cannot be empty"_s);
return setMemberProp<QString>(&ModelProviderCustom::m_name, "name", std::move(value));
}
auto ModelProviderCustom::persist() -> DataStoreResult<>
{
if (auto res = m_store->create(asData()); !res)
@ -112,10 +119,10 @@ void ProviderRegistry::load()
auto registerListener = [this](ModelProvider *provider) {
// listen for any change in the provider so we can tell the model about it
if (auto *mut = dynamic_cast<ModelProviderMutable *>(provider))
connect(mut->asQObject(), "apiKeyChanged", this, "onProviderChanged");
connect(mut->asQObject(), SIGNAL(apiKeyChanged (const QString &)), this, SLOT(onProviderChanged()));
if (auto *cust = dynamic_cast<ModelProviderCustom *>(provider)) {
connect(cust->asQObject(), "nameChanged", this, "onProviderChanged");
connect(cust->asQObject(), "baseUrlChanged", this, "onProviderChanged");
connect(cust->asQObject(), SIGNAL(nameChanged (const QString &)), this, SLOT(onProviderChanged()));
connect(cust->asQObject(), SIGNAL(baseUrlChanged(const QUrl &)), this, SLOT(onProviderChanged()));
}
};
for (auto &p : s_builtinProviders) { // (not all builtin providers are stored)
@ -273,8 +280,8 @@ void ProviderList::onAboutToBeCleared()
bool ProviderListSort::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
auto *leftData = sourceModel()->data(left ).value<ModelProvider *>();
auto *rightData = sourceModel()->data(right).value<ModelProvider *>();
auto *leftData = dynamic_cast<ModelProvider *>(sourceModel()->data(left ).value<QObject *>());
auto *rightData = dynamic_cast<ModelProvider *>(sourceModel()->data(right).value<QObject *>());
if (leftData && rightData) {
if (leftData->isBuiltin() != rightData->isBuiltin())
return leftData->isBuiltin() > rightData->isBuiltin(); // builtins first

View File

@ -61,11 +61,15 @@ concept is_expected = is_expected_impl<std::remove_cvref_t<T>>::value;
/// Drop the type and error information from a QCoro::Task<DataOrRespErr<T>> so it can be used by QML.
template <typename C, typename F, typename... Args>
requires (!detail::is_expected<typename std::invoke_result_t<F, C *, Args...>::value_type>)
QCoro::QmlTask wrapQmlTask(std::shared_ptr<C> c, F f, QString prefix, Args &&...args);
QCoro::QmlTask wrapQmlTask(C *obj, F f, QString prefix, Args &&...args);
template <typename C, typename F, typename... Args>
requires detail::is_expected<typename std::invoke_result_t<F, C *, Args...>::value_type>
QCoro::QmlTask wrapQmlTask(std::shared_ptr<C> c, F f, QString prefix, Args &&...args);
QCoro::QmlTask wrapQmlTask(C *obj, F f, QString prefix, Args &&...args);
/// Drop the error information from a DataOrRespErr<T> so it can be used by QML.
template <typename C, typename F, typename... Args>
bool wrapQmlFunc(C *obj, F &&f, QStringView prefix, Args &&...args);
enum class GenerationParam {
NPredict,
@ -182,7 +186,7 @@ protected:
template <typename T, typename S, typename C>
[[nodiscard]] DataStoreResult<> setMemberProp(this S &self, T C::* member, std::string_view name, T value,
std::optional<QString> createName = {});
bool create = false);
[[nodiscard]] virtual bool persisted() const { return true; }
@ -198,11 +202,16 @@ public:
bool isBuiltin() const final { return false; }
// setters
[[nodiscard]] DataStoreResult<> setName (QString value)
{ return setMemberProp<QString>(&ModelProviderCustom::m_name, "name", std::move(value)); }
[[nodiscard]] DataStoreResult<> setName (QString value);
[[nodiscard]] DataStoreResult<> setBaseUrl(QUrl value)
{ return setMemberProp<QUrl >(&ModelProviderCustom::m_baseUrl, "baseUrl", std::move(value)); }
// QML setters
Q_INVOKABLE bool setNameQml (QString value)
{ return wrapQmlFunc(this, &ModelProviderCustom::setName, u"setName", std::move(value)); }
Q_INVOKABLE bool setBaseUrlQml(QString value)
{ return wrapQmlFunc(this, &ModelProviderCustom::setBaseUrl, u"setBaseUrl", std::move(value)); }
[[nodiscard]] auto persist() -> DataStoreResult<>;
protected:
@ -264,7 +273,7 @@ private:
ProviderStore m_customStore;
ProviderStore m_builtinStore;
std::unordered_map<QUuid, std::shared_ptr<ModelProvider>> m_providers;
std::vector<const QUuid *> m_providerList; // TODO: implement
std::vector<const QUuid *> m_providerList;
};
class ProviderList : public QAbstractListModel {
@ -296,7 +305,7 @@ class ProviderListSort : public QSortFilterProxyModel {
QML_ELEMENT
private:
explicit ProviderListSort() { setSourceModel(&m_model); }
explicit ProviderListSort() { setSourceModel(&m_model); sort(0); }
public:
static ProviderListSort *create(QQmlEngine *, QJSEngine *) { return new ProviderListSort(); }

View File

@ -38,6 +38,14 @@ QCoro::QmlTask wrapQmlTask(C *obj, F f, QString prefix, Args &&...args)
}(std::move(ptr), std::move(f), std::move(prefix), std::forward<Args>(args)...);
}
template <typename C, typename F, typename... Args>
bool wrapQmlFunc(C *obj, F &&f, QStringView prefix, Args &&...args)
{
auto res = std::invoke(std::forward<F>(f), obj, std::forward<Args>(args)...);
if (!res) { qWarning().noquote() << prefix << "failed:" << res.error().errorString(); }
return bool(res);
}
template <typename T, typename S, typename C>
void GenerationParams::tryParseValue(this S &self, QMap<GenerationParam, QVariant> &values, GenerationParam key,
T C::* dest)
@ -48,15 +56,15 @@ void GenerationParams::tryParseValue(this S &self, QMap<GenerationParam, QVarian
template <typename T, typename S, typename C>
auto ModelProviderMutable::setMemberProp(this S &self, T C::* member, std::string_view name, T value,
std::optional<QString> createName) -> DataStoreResult<>
bool create) -> DataStoreResult<>
{
auto &mpc = static_cast<ModelProviderMutable &>(self);
auto &mpm = static_cast<ModelProviderMutable &>(self);
auto &cur = self.*member;
if (cur != value) {
cur = std::move(value);
if (mpc.persisted()) {
auto data = mpc.asData();
if (auto res = mpc.m_store->setData(std::move(data), createName); !res)
if (mpm.persisted()) {
auto data = mpm.asData();
if (auto res = mpm.m_store->setData(std::move(data), mpm.name(), create); !res)
return res;
}
QMetaObject::invokeMethod(self.asQObject(), fmt::format("{}Changed", name).c_str(), cur);

View File

@ -82,7 +82,7 @@ auto DataStoreBase::reload() -> DataStoreResult<>
}
for (auto &entry : it) {
if (!entry.is_regular_file())
if (!entry.is_regular_file() || entry.path().extension() != ".json")
continue; // skip directories and such
file.setFileName(entry.path());
if (!file.open(QFile::ReadOnly)) {
@ -93,7 +93,7 @@ auto DataStoreBase::reload() -> DataStoreResult<>
if (!jv) {
(qWarning().nospace() << "skipping " << file.fileName() << " because of read error: ").noquote()
<< jv.error().errorString();
} else if (auto [unique, uuid] = cacheInsert(*jv); !unique)
} else if (auto [unique, uuid] = cacheInsert(entry.path().stem(), *jv); !unique)
qWarning() << "skipping duplicate data store entry:" << uuid;
file.close();
}
@ -109,12 +109,23 @@ auto DataStoreBase::setPath(fs::path path) -> DataStoreResult<>
return {};
}
auto DataStoreBase::getFilePath(const QString &name) -> fs::path
{ return m_path / fmt::format("{}.json", QLatin1StringView(normalizeName(name))); }
auto DataStoreBase::openNew(const QString &name) -> DataStoreResult<std::unique_ptr<QFile>>
QByteArray DataStoreBase::normalizeName(const QString &name)
{
auto path = getFilePath(name);
auto lower = name.toLower();
auto norm = QUrl::toPercentEncoding(lower, /*exclude*/ " !#$%&'()+,;=@[]^`{}"_ba, /*include*/ "~"_ba);
// leading dot indicates a hidden file on Unix
if (norm.startsWith('.'))
norm = "%2E"_ba.append(QByteArrayView(norm).slice(1));
return norm;
}
auto DataStoreBase::getFilePath(const QByteArray &normName) -> fs::path
{ return m_path / fmt::format("{}.json", QLatin1StringView(normName)); }
auto DataStoreBase::openNew(const QByteArray &normName) -> DataStoreResult<std::unique_ptr<QFile>>
{
auto path = getFilePath(normName);
auto file = std::make_unique<QFile>(path);
if (file->exists())
return std::unexpected(sys::system_error(std::make_error_code(std::errc::file_exists), path.string()));
@ -123,9 +134,9 @@ auto DataStoreBase::openNew(const QString &name) -> DataStoreResult<std::unique_
return file;
}
auto DataStoreBase::openExisting(const QString &name, bool allowCreate) -> DataStoreResult<std::unique_ptr<QSaveFile>>
auto DataStoreBase::openExisting(const QByteArray &normName, bool allowCreate) -> DataStoreResult<std::unique_ptr<QSaveFile>>
{
auto path = getFilePath(name);
auto path = getFilePath(normName);
if (!allowCreate && !QFile::exists(path))
return std::unexpected(sys::system_error(
std::make_error_code(std::errc::no_such_file_or_directory), path.string()
@ -213,16 +224,5 @@ auto DataStoreBase::write(const json::value &value, QFileDevice &file) -> DataSt
return {};
}
QByteArray DataStoreBase::normalizeName(const QString &name)
{
auto lower = name.toLower();
auto norm = QUrl::toPercentEncoding(lower, /*exclude*/ " !#$%&'()+,;=@[]^`{}"_ba, /*include*/ "~"_ba);
// "." and ".." are special filenames
return norm == "."_ba ? "%2E"_ba :
norm == ".."_ba ? "%2E%2E"_ba :
norm;
}
} // namespace gpt4all::ui

View File

@ -70,21 +70,20 @@ public:
protected:
auto reload() -> DataStoreResult<>;
virtual auto clear() -> DataStoreResult<> = 0;
struct CacheInsertResult { bool unique; QUuid uuid; };
virtual CacheInsertResult cacheInsert(const boost::json::value &jv) = 0;
struct CacheInsertResult { bool unique; QUuid id; };
virtual CacheInsertResult cacheInsert(const std::filesystem::path &stem, const boost::json::value &jv) = 0;
// helpers
auto getFilePath(const QString &name) -> std::filesystem::path;
auto openNew(const QString &name) -> DataStoreResult<std::unique_ptr<QFile>>;
auto openExisting(const QString &name, bool allowCreate = false) -> DataStoreResult<std::unique_ptr<QSaveFile>>;
static QByteArray normalizeName(const QString &name);
auto getFilePath(const QByteArray &normName) -> std::filesystem::path;
auto openNew(const QByteArray &normName) -> DataStoreResult<std::unique_ptr<QFile>>;
auto openExisting(const QByteArray &normName, bool allowCreate = false) -> DataStoreResult<std::unique_ptr<QSaveFile>>;
static auto read(QFileDevice &file, boost::json::stream_parser &parser) -> DataStoreResult<boost::json::value>;
auto write(const boost::json::value &value, QFileDevice &file) -> DataStoreResult<>;
private:
static constexpr uint JSON_BUFSIZ = 16384; // default QFILE_WRITEBUFFER_SIZE
static QByteArray normalizeName(const QString &name);
protected:
std::filesystem::path m_path;
@ -98,7 +97,7 @@ public:
explicit DataStore(std::filesystem::path path);
auto list() { return m_entries | std::views::transform([](auto &e) { return e.second; }); }
auto setData(T data, std::optional<QString> createName = {}) -> DataStoreResult<>;
auto setData(T data, const QString &name, bool create = false) -> DataStoreResult<>;
auto remove(const QUuid &id) -> DataStoreResult<>;
auto acquire(QUuid id) -> DataStoreResult<std::optional<const T *>>;
@ -112,12 +111,13 @@ public:
protected:
auto createImpl(T data, const QString &name) -> DataStoreResult<>;
auto clear() -> DataStoreResult<> final;
CacheInsertResult cacheInsert(const boost::json::value &jv) override;
CacheInsertResult cacheInsert(const std::filesystem::path &stem, const boost::json::value &jv) override;
private:
std::unordered_map<QUuid, T> m_entries;
std::unordered_set<QUuid> m_acquired;
std::unordered_map<QUuid, QString> m_names;
std::unordered_map<QUuid, T > m_entries;
std::unordered_map<QUuid, QByteArray> m_normNames;
std::unordered_map<QByteArray, QUuid > m_normNameToId;
std::unordered_set<QUuid> m_acquired;
};

View File

@ -3,13 +3,13 @@
#include <boost/json.hpp> // IWYU pragma: keep
#include <gpt4all-backend/json-helpers.h> // IWYU pragma: keep
#include <QDebug>
#include <QSaveFile>
#include <QtAssert>
#include <QtLogging>
#include <ranges>
#include <system_error>
namespace views = std::views;
#include <algorithm>
#include <tuple>
namespace gpt4all::ui {
@ -26,35 +26,74 @@ DataStore<T>::DataStore(std::filesystem::path path)
template <typename T>
auto DataStore<T>::createImpl(T data, const QString &name) -> DataStoreResult<>
{
// acquire id
typename decltype(m_entries)::iterator entry;
bool unique;
std::tie(entry, unique) = m_entries.emplace(data.id, std::move(data));
if (!unique)
return std::unexpected(QStringLiteral("id not unique: %1").arg(data.id.toString()));
auto *pdata = &entry->second;
auto normName = normalizeName(name);
auto [nameIt, unique2] = m_normNames.emplace(pdata->id, normName);
Q_UNUSED(unique2)
Q_ASSERT(unique2);
// acquire name
typename decltype(m_normNameToId)::iterator n2idIt;
std::tie(n2idIt, unique) = m_normNameToId.emplace(std::move(normName), pdata->id);
if (!unique) {
m_entries.erase(entry);
m_normNames.erase(nameIt);
return std::unexpected(QStringLiteral("name not unique: %1").arg(QLatin1StringView(normName)));
}
// acquire path
auto file = openNew(name);
if (!file)
auto file = openNew(normName);
if (!file) {
m_entries.erase(entry);
m_normNames.erase(nameIt);
m_normNameToId.erase(n2idIt);
return std::unexpected(file.error());
}
// serialize
if (auto res = write(boost::json::value_from(data), **file); !res)
if (auto res = write(boost::json::value_from(*pdata), **file); !res) {
m_entries.erase(entry);
m_normNames.erase(nameIt);
m_normNameToId.erase(n2idIt);
if (!(*file)->remove())
(qWarning().nospace() << "failed to remove " << (*file)->fileName()).noquote() << ": " << (*file)->errorString();
return std::unexpected(res.error());
// insert
auto [it, unique] = m_entries.emplace(data.id, std::move(data));
Q_ASSERT(unique);
}
return {};
}
template <typename T>
auto DataStore<T>::setData(T data, std::optional<QString> createName) -> DataStoreResult<>
auto DataStore<T>::setData(T data, const QString &name, bool create) -> DataStoreResult<>
{
const QString *openName;
auto name_it = m_names.find(data.id);
if (name_it != m_names.end()) {
openName = &name_it->second;
} else if (createName) {
openName = &*createName;
// acquire name
auto normName = normalizeName(name);
auto n2idIt = m_normNameToId.find(normName);
if (n2idIt != m_normNameToId.end() && n2idIt->second != data.id)
return std::unexpected(QStringLiteral("name is not unique: %1").arg(QLatin1StringView(normName)));
// determine filename to open
bool isNew = false;
QByteArray *openName;
auto nameIt = m_normNames.find(data.id);
if (nameIt != m_normNames.end()) {
openName = &nameIt->second;
} else if (create) {
isNew = true;
openName = &normName;
} else
return std::unexpected(QStringLiteral("id not found: %1").arg(data.id.toString()));
bool isRename = !isNew && n2idIt == m_normNameToId.end();
// acquire path
auto file = openExisting(*openName, !!createName);
auto file = openExisting(*openName, create);
if (!file)
return std::unexpected(file.error());
@ -64,21 +103,33 @@ auto DataStore<T>::setData(T data, std::optional<QString> createName) -> DataSto
if (!(*file)->commit())
return std::unexpected(file->get());
// update
m_entries[data.id] = std::move(data);
// rename if necessary
if (name_it == m_names.end()) {
m_names.emplace(data.id, std::move(*createName));
} else if (*createName != name_it->second) {
std::error_code ec;
auto newPath = getFilePath(*createName);
std::filesystem::rename(getFilePath(name_it->second), newPath, ec);
if (ec)
return std::unexpected(ec);
m_names.at(data.id) = std::move(*createName);
// update cache
auto id = data.id;
if (isNew) {
[[maybe_unused]] bool unique;
std::tie(std::ignore, unique) = m_entries .emplace(data.id, std::move(data));
Q_ASSERT(unique);
std::tie(std::ignore, unique) = m_normNames.emplace(data.id, normName );
Q_ASSERT(unique);
} else {
m_entries.at(data.id) = std::move(data);
nameIt->second = normName;
}
// update name
if (isRename) {
[[maybe_unused]] auto nRemoved = m_normNameToId.erase(*openName); // remove old name
Q_ASSERT(nRemoved);
std::error_code ec;
std::filesystem::rename(getFilePath(*openName), getFilePath(normName), ec);
if (ec)
return std::unexpected(ec); // FIXME(jared): attempt rollback? the state is now inconsistent.
}
if (isNew || isRename) {
auto [_, unique] = m_normNameToId.emplace(*openName, id);
Q_ASSERT(unique);
}
return {};
}
@ -86,20 +137,24 @@ template <typename T>
auto DataStore<T>::remove(const QUuid &id) -> DataStoreResult<>
{
// acquire UUID
auto it = m_entries.find(id);
if (it == m_entries.end())
auto nameIt = m_normNames.find(id);
if (nameIt == m_normNames.end())
return std::unexpected(QStringLiteral("id not found: %1").arg(id.toString()));
auto &[_, data] = *it;
// remove the path
auto path = getFilePath(data.name);
auto path = getFilePath(nameIt->second);
QFile file(path);
if (!file.remove())
throw std::unexpected(&file);
// update cache
m_entries.erase(it);
auto normName = std::move(nameIt->second);
m_normNames.erase(nameIt);
[[maybe_unused]] size_t nRemoved;
nRemoved = m_entries.erase(id);
Q_ASSERT(nRemoved);
nRemoved = m_normNameToId.erase(normName);
Q_ASSERT(nRemoved);
return {};
}
@ -126,16 +181,27 @@ auto DataStore<T>::clear() -> DataStoreResult<>
if (!m_acquired.empty())
return std::unexpected(QStringLiteral("cannot clear data store with living references"));
m_entries.clear();
m_normNameToId.clear();
return {};
}
template <typename T>
auto DataStore<T>::cacheInsert(const boost::json::value &jv) -> CacheInsertResult
auto DataStore<T>::cacheInsert(const std::filesystem::path &stem, const boost::json::value &jv) -> CacheInsertResult
{
auto data = boost::json::value_to<T>(jv);
auto id = data.id;
auto [_, ok] = m_entries.emplace(id, std::move(data));
return { ok, std::move(id) };
auto [entryIt, unique] = m_entries.emplace(id, std::move(data));
if (!unique)
return { .unique = false, .id = std::move(id) };
auto normName = toQString(stem).toUtf8(); // FIXME(jared): better not to trust the filename
std::tie(std::ignore, unique) = m_normNameToId.emplace(normName, id);
if (!unique) {
m_entries.erase(entryIt);
return { .unique = false, .id = std::move(id) };
}
std::tie(std::ignore, unique) = m_normNames.emplace(id, std::move(normName));
Q_ASSERT(unique);
return { .unique = true, .id = std::move(id) };
}