323 lines
9.5 KiB
C++
323 lines
9.5 KiB
C++
#include "storage/state_repository.hpp"
|
|
|
|
#include "domain/layout_profile.hpp"
|
|
#include "domain/layout_schema.hpp"
|
|
#include "util/file_util.hpp"
|
|
#include "util/id_util.hpp"
|
|
#include "util/string_util.hpp"
|
|
|
|
namespace lm {
|
|
|
|
std::filesystem::path StateRepository::modelsDir() const
|
|
{
|
|
return app_.data_path.parent_path() / "models";
|
|
}
|
|
|
|
std::filesystem::path StateRepository::profileFilePath(const std::string& id) const
|
|
{
|
|
return modelsDir() / (id + ".json");
|
|
}
|
|
|
|
std::optional<nlohmann::json> StateRepository::loadProfileFromDisk(const std::string& id) const
|
|
{
|
|
const auto raw = FileUtil::readBinary(profileFilePath(id));
|
|
if (raw.empty())
|
|
return std::nullopt;
|
|
try
|
|
{
|
|
return nlohmann::json::parse(raw);
|
|
}
|
|
catch (...)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
bool StateRepository::saveProfileToDisk(const nlohmann::json& profile) const
|
|
{
|
|
if (!profile.is_object() || !profile.contains("id") || !profile["id"].is_string())
|
|
return false;
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(modelsDir(), ec);
|
|
auto body = profile.dump(2);
|
|
body.push_back('\n');
|
|
return FileUtil::writeBinaryAtomic(profileFilePath(profile["id"].get<std::string>()), body);
|
|
}
|
|
|
|
bool StateRepository::deleteProfileFile(const std::string& id) const
|
|
{
|
|
std::error_code ec;
|
|
std::filesystem::remove(profileFilePath(id), ec);
|
|
return true;
|
|
}
|
|
|
|
void StateRepository::loadActiveCache()
|
|
{
|
|
nlohmann::json& state = app_.state;
|
|
const auto idx = LayoutProfile::findActiveIndex(state);
|
|
if (!idx)
|
|
return;
|
|
|
|
const std::string id = state["layouts"][*idx]["id"].get<std::string>();
|
|
nlohmann::json profile;
|
|
if (auto loaded = loadProfileFromDisk(id))
|
|
profile = *loaded;
|
|
else
|
|
{
|
|
profile = LayoutProfile::make(state["layouts"][*idx]["name"].get<std::string>(),
|
|
LayoutSchema::defaultLayoutObject(),
|
|
nlohmann::json::array());
|
|
profile["id"] = id;
|
|
if (state["layouts"][*idx].contains("created_at"))
|
|
profile["created_at"] = state["layouts"][*idx]["created_at"];
|
|
}
|
|
|
|
if (!profile.contains("layout") || !profile["layout"].is_object())
|
|
profile["layout"] = LayoutSchema::defaultLayoutObject();
|
|
if (!profile.contains("lidars") || !profile["lidars"].is_array())
|
|
profile["lidars"] = nlohmann::json::array();
|
|
if (!profile.contains("imus") || !profile["imus"].is_array())
|
|
profile["imus"] = nlohmann::json::array();
|
|
|
|
LayoutSchema::ensure(profile["layout"]);
|
|
state["layout"] = profile["layout"];
|
|
state["lidars"] = profile["lidars"];
|
|
state["imus"] = profile["imus"];
|
|
}
|
|
|
|
bool StateRepository::persistActiveProfile()
|
|
{
|
|
nlohmann::json& state = app_.state;
|
|
const auto idx = LayoutProfile::findActiveIndex(state);
|
|
if (!idx)
|
|
return false;
|
|
|
|
auto& entry = state["layouts"][*idx];
|
|
nlohmann::json profile;
|
|
profile["id"] = entry["id"];
|
|
profile["name"] = entry.contains("name") ? entry["name"] : nlohmann::json("Layout");
|
|
profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601());
|
|
profile["updated_at"] = IdUtil::nowIso8601();
|
|
profile["layout"] = state["layout"];
|
|
profile["lidars"] = state["lidars"];
|
|
profile["imus"] = state.contains("imus") && state["imus"].is_array() ? state["imus"] : nlohmann::json::array();
|
|
LayoutSchema::ensure(profile["layout"]);
|
|
if (!saveProfileToDisk(profile))
|
|
return false;
|
|
entry = LayoutProfile::catalogEntryFromProfile(profile);
|
|
return true;
|
|
}
|
|
|
|
nlohmann::json StateRepository::globalStateForDisk(const nlohmann::json& state) const
|
|
{
|
|
nlohmann::json out = nlohmann::json::object();
|
|
out["version"] = 3;
|
|
if (state.contains("active_layout_id"))
|
|
out["active_layout_id"] = state["active_layout_id"];
|
|
out["layouts"] = nlohmann::json::array();
|
|
if (state.contains("layouts") && state["layouts"].is_array())
|
|
{
|
|
for (const auto& entry : state["layouts"])
|
|
{
|
|
if (!entry.is_object() || !entry.contains("id") || !entry.contains("name"))
|
|
continue;
|
|
if (entry.contains("layout"))
|
|
out["layouts"].push_back(LayoutProfile::catalogEntryFromProfile(entry));
|
|
else
|
|
out["layouts"].push_back(entry);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void StateRepository::stripInlineProfiles(nlohmann::json& state) const
|
|
{
|
|
if (!state.contains("layouts") || !state["layouts"].is_array())
|
|
return;
|
|
nlohmann::json catalog = nlohmann::json::array();
|
|
for (const auto& entry : state["layouts"])
|
|
{
|
|
if (!entry.is_object() || !entry.contains("id"))
|
|
continue;
|
|
if (entry.contains("layout"))
|
|
catalog.push_back(LayoutProfile::catalogEntryFromProfile(entry));
|
|
else
|
|
catalog.push_back(entry);
|
|
}
|
|
state["layouts"] = catalog;
|
|
}
|
|
|
|
void StateRepository::migrateStorage()
|
|
{
|
|
nlohmann::json& s = app_.state;
|
|
if (!s.is_object())
|
|
s = nlohmann::json::object();
|
|
|
|
const int version = s.contains("version") && s["version"].is_number_integer() ? s["version"].get<int>() : 1;
|
|
|
|
if (!s.contains("layouts") || !s["layouts"].is_array() || s["layouts"].empty())
|
|
{
|
|
nlohmann::json layout = s.contains("layout") && s["layout"].is_object() ? s["layout"] : LayoutSchema::defaultLayoutObject();
|
|
nlohmann::json lidars = s.contains("lidars") && s["lidars"].is_array() ? s["lidars"] : nlohmann::json::array();
|
|
nlohmann::json profile = LayoutProfile::make("Mặc định", layout, lidars);
|
|
LayoutSchema::ensure(profile["layout"]);
|
|
saveProfileToDisk(profile);
|
|
s["layouts"] = nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)});
|
|
s["active_layout_id"] = profile["id"].get<std::string>();
|
|
}
|
|
else if (version < 3)
|
|
{
|
|
if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() ||
|
|
!LayoutProfile::findIndex(s, s["active_layout_id"].get<std::string>()))
|
|
{
|
|
s["active_layout_id"] = s["layouts"][0]["id"].get<std::string>();
|
|
}
|
|
|
|
nlohmann::json catalog = nlohmann::json::array();
|
|
for (auto& entry : s["layouts"])
|
|
{
|
|
if (!entry.is_object() || !entry.contains("id"))
|
|
continue;
|
|
nlohmann::json profile;
|
|
if (entry.contains("layout"))
|
|
{
|
|
profile = entry;
|
|
if (!profile.contains("name"))
|
|
profile["name"] = "Layout";
|
|
if (!profile.contains("lidars") || !profile["lidars"].is_array())
|
|
profile["lidars"] = nlohmann::json::array();
|
|
if (!profile.contains("created_at"))
|
|
profile["created_at"] = IdUtil::nowIso8601();
|
|
LayoutProfile::touch(profile);
|
|
LayoutSchema::ensure(profile["layout"]);
|
|
saveProfileToDisk(profile);
|
|
catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile));
|
|
}
|
|
else
|
|
{
|
|
const std::string id = entry["id"].get<std::string>();
|
|
if (auto loaded = loadProfileFromDisk(id))
|
|
{
|
|
catalog.push_back(LayoutProfile::catalogEntryFromProfile(*loaded));
|
|
}
|
|
else
|
|
{
|
|
profile = LayoutProfile::make(entry["name"].get<std::string>(), LayoutSchema::defaultLayoutObject(), nlohmann::json::array());
|
|
profile["id"] = id;
|
|
profile["created_at"] = entry.value("created_at", IdUtil::nowIso8601());
|
|
LayoutProfile::touch(profile);
|
|
saveProfileToDisk(profile);
|
|
catalog.push_back(LayoutProfile::catalogEntryFromProfile(profile));
|
|
}
|
|
}
|
|
}
|
|
s["layouts"] = catalog;
|
|
}
|
|
else
|
|
{
|
|
stripInlineProfiles(s);
|
|
if (!s.contains("active_layout_id") || !s["active_layout_id"].is_string() ||
|
|
!LayoutProfile::findIndex(s, s["active_layout_id"].get<std::string>()))
|
|
{
|
|
s["active_layout_id"] = s["layouts"][0]["id"].get<std::string>();
|
|
}
|
|
}
|
|
|
|
s["version"] = 3;
|
|
s.erase("layout");
|
|
s.erase("lidars");
|
|
loadActiveCache();
|
|
}
|
|
|
|
void StateRepository::bootstrapDefaultState()
|
|
{
|
|
const nlohmann::json layout = LayoutSchema::defaultLayoutObject();
|
|
nlohmann::json profile = LayoutProfile::make("Mặc định", layout, nlohmann::json::array());
|
|
LayoutSchema::ensure(profile["layout"]);
|
|
saveProfileToDisk(profile);
|
|
app_.state = nlohmann::json{{"version", 3},
|
|
{"active_layout_id", profile["id"]},
|
|
{"layouts", nlohmann::json::array({LayoutProfile::catalogEntryFromProfile(profile)})}};
|
|
app_.state["layout"] = profile["layout"];
|
|
app_.state["lidars"] = profile["lidars"];
|
|
app_.state["imus"] = profile.contains("imus") ? profile["imus"] : nlohmann::json::array();
|
|
}
|
|
|
|
StateRepository::StateRepository(std::filesystem::path data_path)
|
|
{
|
|
app_.data_path = std::move(data_path);
|
|
}
|
|
|
|
bool StateRepository::load()
|
|
{
|
|
const auto raw = FileUtil::readBinary(app_.data_path);
|
|
if (raw.empty())
|
|
{
|
|
bootstrapDefaultState();
|
|
save();
|
|
return true;
|
|
}
|
|
try
|
|
{
|
|
app_.state = nlohmann::json::parse(raw);
|
|
ensureSchema();
|
|
save();
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
bootstrapDefaultState();
|
|
save();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void StateRepository::ensureSchema()
|
|
{
|
|
migrateStorage();
|
|
}
|
|
|
|
bool StateRepository::saveProfile(const nlohmann::json& profile)
|
|
{
|
|
return saveProfileToDisk(profile);
|
|
}
|
|
|
|
void StateRepository::reloadActiveCache()
|
|
{
|
|
loadActiveCache();
|
|
}
|
|
|
|
bool StateRepository::deleteProfile(const std::string& id)
|
|
{
|
|
return deleteProfileFile(id);
|
|
}
|
|
|
|
std::optional<nlohmann::json> StateRepository::loadProfileById(const std::string& id) const
|
|
{
|
|
return loadProfileFromDisk(id);
|
|
}
|
|
|
|
bool StateRepository::saveAppState()
|
|
{
|
|
if (!persistActiveProfile())
|
|
return false;
|
|
return save();
|
|
}
|
|
|
|
bool StateRepository::save() const
|
|
{
|
|
try
|
|
{
|
|
const nlohmann::json disk = globalStateForDisk(app_.state);
|
|
auto raw = disk.dump(2);
|
|
raw.push_back('\n');
|
|
return FileUtil::writeBinaryAtomic(app_.data_path, raw);
|
|
}
|
|
catch (...)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace lm
|