Files
Crosspoint/src/OpdsServerStore.cpp
T
2026-04-23 19:45:38 +02:00

172 lines
4.8 KiB
C++

#include "OpdsServerStore.h"
#include <HalStorage.h>
#include <JsonSettingsIO.h>
#include <Logging.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include "CrossPointSettings.h"
#include "util/UrlUtils.h"
OpdsServerStore OpdsServerStore::instance;
namespace {
constexpr char OPDS_FILE_JSON[] = "/.crosspoint/opds.json";
bool containsWhitespace(const std::string& value) {
return std::any_of(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
}
} // namespace
namespace OpdsServerValidation {
std::optional<std::string> normalizeUrl(const std::string& url) {
if (url.empty() || containsWhitespace(url)) {
return std::nullopt;
}
std::string normalized = url;
if (normalized.find("://") == std::string::npos) {
normalized = "https://" + normalized;
}
const bool hasHttpScheme = normalized.rfind("http://", 0) == 0 || normalized.rfind("https://", 0) == 0;
if (!hasHttpScheme || UrlUtils::extractHostname(normalized).empty()) {
return std::nullopt;
}
return normalized;
}
} // namespace OpdsServerValidation
bool OpdsServerStore::saveToFile() const {
Storage.mkdir("/.crosspoint");
return JsonSettingsIO::saveOpds(*this, OPDS_FILE_JSON);
}
bool OpdsServerStore::loadFromFile() {
if (Storage.exists(OPDS_FILE_JSON)) {
String json = Storage.readFile(OPDS_FILE_JSON);
if (json.isEmpty()) {
LOG_ERR("OPS", "Failed to parse %s", OPDS_FILE_JSON);
return false;
}
// resave flag is set when passwords were stored in plaintext and need re-obfuscation
bool resave = false;
bool result = JsonSettingsIO::loadOpds(*this, json.c_str(), &resave);
if (!result) {
LOG_ERR("OPS", "Failed to parse %s", OPDS_FILE_JSON);
return false;
}
if (resave) {
LOG_DBG("OPS", "Resaving JSON with obfuscated passwords");
if (!saveToFile()) {
LOG_ERR("OPS", "Failed to resave %s after password migration", OPDS_FILE_JSON);
}
}
return true;
}
// No opds.json found — attempt one-time migration from the legacy single-server
// fields in CrossPointSettings (opdsServerUrl/opdsUsername/opdsPassword).
if (migrateFromSettings()) {
LOG_DBG("OPS", "Migrated legacy OPDS settings");
return true;
}
return false;
}
bool OpdsServerStore::migrateFromSettings() {
if (strlen(SETTINGS.opdsServerUrl) == 0) {
return false;
}
OpdsServer server;
server.name = "OPDS Server";
server.url = SETTINGS.opdsServerUrl;
server.username = SETTINGS.opdsUsername;
server.password = SETTINGS.opdsPassword;
servers.push_back(std::move(server));
if (saveToFile()) {
// Clear legacy fields so migration won't run again on next boot
SETTINGS.opdsServerUrl[0] = '\0';
SETTINGS.opdsUsername[0] = '\0';
SETTINGS.opdsPassword[0] = '\0';
SETTINGS.saveToFile();
LOG_DBG("OPS", "Migrated single-server OPDS config to opds.json");
return true;
}
// Save failed — roll back in-memory state so we don't have a partial migration
servers.clear();
return false;
}
std::optional<size_t> OpdsServerStore::addServer(const OpdsServer& server) {
if (servers.size() >= MAX_SERVERS) {
LOG_DBG("OPS", "Cannot add more servers, limit of %zu reached", MAX_SERVERS);
return std::nullopt;
}
const auto originalServers = servers;
servers.push_back(server);
if (!saveToFile()) {
servers = originalServers;
LOG_ERR("OPS", "Failed to persist added server, rolled back in-memory state");
return std::nullopt;
}
const size_t insertedIndex = servers.size() - 1;
LOG_DBG("OPS", "Added server at index %zu: %s", insertedIndex, server.name.c_str());
return insertedIndex;
}
bool OpdsServerStore::updateServer(size_t index, const OpdsServer& server) {
if (index >= servers.size()) {
return false;
}
const auto originalServers = servers;
servers[index] = server;
if (!saveToFile()) {
servers = originalServers;
LOG_ERR("OPS", "Failed to persist updated server at index %zu, rolled back in-memory state", index);
return false;
}
LOG_DBG("OPS", "Updated server at index %zu: %s", index, server.name.c_str());
return true;
}
bool OpdsServerStore::removeServer(size_t index) {
if (index >= servers.size()) {
return false;
}
const auto originalServers = servers;
const std::string removedName = servers[index].name;
servers.erase(servers.begin() + static_cast<ptrdiff_t>(index));
if (!saveToFile()) {
servers = originalServers;
LOG_ERR("OPS", "Failed to persist removed server at index %zu, rolled back in-memory state", index);
return false;
}
LOG_DBG("OPS", "Removed server at index %zu: %s", index, removedName.c_str());
return true;
}
const OpdsServer* OpdsServerStore::getServer(size_t index) const {
if (index >= servers.size()) {
return nullptr;
}
return &servers[index];
}