commits
tags
#include "torlinkc/engine/persist.hpp"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <nlohmann/json.hpp>
#include "torlinkc/config/paths.hpp"
#include "torlinkc/util/atomic_write.hpp"
namespace fs = std::filesystem;
using nlohmann::json;
namespace torlinkc {
namespace {
json toJson(const QueueItem& it) {
json j;
j["id"] = it.id;
j["name"] = it.name;
if (!it.source.empty()) j["source"] = it.source;
j["magnet"] = it.magnet;
j["dir"] = it.dir;
j["status"] = downloadStatusToString(it.status);
j["progress"] = it.progress;
j["totalBytes"] = it.totalBytes;
j["downloadedBytes"] = it.downloadedBytes;
j["speed"] = it.speed;
j["peers"] = it.peers;
if (it.eta) j["eta"] = *it.eta;
if (it.files) j["files"] = *it.files;
if (it.error) j["error"] = *it.error;
j["addedAt"] = it.addedAt;
return j;
}
// Mirrors persist.ts's isQueueItem: the only load-bearing check is that id and
// magnet are both strings. Everything else is read defensively field-by-field
// below rather than rejecting the whole entry.
std::optional<QueueItem> fromJson(const json& j) {
if (!j.is_object()) return std::nullopt;
auto idIt = j.find("id");
auto magnetIt = j.find("magnet");
if (idIt == j.end() || !idIt->is_string()) return std::nullopt;
if (magnetIt == j.end() || !magnetIt->is_string()) return std::nullopt;
QueueItem it;
it.id = idIt->get<std::string>();
it.magnet = magnetIt->get<std::string>();
if (auto v = j.find("name"); v != j.end() && v->is_string()) it.name = v->get<std::string>();
if (auto v = j.find("source"); v != j.end() && v->is_string()) it.source = v->get<std::string>();
if (auto v = j.find("dir"); v != j.end() && v->is_string()) it.dir = v->get<std::string>();
it.status = DownloadStatus::Downloading;
if (auto v = j.find("status"); v != j.end() && v->is_string()) {
if (auto parsed = downloadStatusFromString(v->get<std::string>())) it.status = *parsed;
}
if (auto v = j.find("progress"); v != j.end() && v->is_number()) it.progress = v->get<int>();
if (auto v = j.find("totalBytes"); v != j.end() && v->is_number()) it.totalBytes = v->get<std::int64_t>();
if (auto v = j.find("downloadedBytes"); v != j.end() && v->is_number())
it.downloadedBytes = v->get<std::int64_t>();
if (auto v = j.find("speed"); v != j.end() && v->is_number()) it.speed = v->get<int>();
if (auto v = j.find("peers"); v != j.end() && v->is_number()) it.peers = v->get<int>();
if (auto v = j.find("eta"); v != j.end() && v->is_number()) it.eta = v->get<double>();
if (auto v = j.find("files"); v != j.end() && v->is_number()) it.files = v->get<int>();
if (auto v = j.find("error"); v != j.end() && v->is_string()) it.error = v->get<std::string>();
if (auto v = j.find("addedAt"); v != j.end() && v->is_number()) it.addedAt = v->get<std::int64_t>();
return it;
}
json toJson(const SeedRecord& r) {
json j;
j["id"] = r.id;
j["status"] = seedStatusToString(r.status);
return j;
}
std::string readFile(const std::string& path) {
std::ifstream in(path, std::ios::binary);
if (!in) return {};
std::ostringstream ss;
ss << in.rdbuf();
return ss.str();
}
} // namespace
void saveQueue(const std::vector<QueueItem>& items) {
json arr = json::array();
for (const auto& it : items) arr.push_back(toJson(it));
try {
writeJsonAtomic(paths::queueFile(), arr);
} catch (...) {
}
}
std::vector<QueueItem> loadQueue() {
std::vector<QueueItem> out;
std::string raw = readFile(paths::queueFile());
if (raw.empty()) return out;
json parsed;
try {
parsed = json::parse(raw);
} catch (...) {
return out;
}
if (!parsed.is_array()) return out;
for (const auto& el : parsed) {
if (auto it = fromJson(el)) out.push_back(std::move(*it));
}
return out;
}
void saveSeeds(const std::vector<SeedRecord>& records) {
json arr = json::array();
for (const auto& r : records) arr.push_back(toJson(r));
try {
writeJsonAtomic(paths::seedsFile(), arr);
} catch (...) {
}
}
std::vector<SeedRecord> loadSeeds() {
std::vector<SeedRecord> out;
std::string raw = readFile(paths::seedsFile());
if (raw.empty()) return out;
json parsed;
try {
parsed = json::parse(raw);
} catch (...) {
return out;
}
if (!parsed.is_array()) return out;
for (const auto& el : parsed) {
// Legacy format was a bare id array; treat each as a seeding entry.
if (el.is_string()) {
out.push_back(SeedRecord{el.get<std::string>(), SeedStatus::Seeding});
continue;
}
if (!el.is_object()) continue;
auto idIt = el.find("id");
auto statusIt = el.find("status");
if (idIt == el.end() || !idIt->is_string()) continue;
if (statusIt == el.end() || !statusIt->is_string()) continue;
const std::string status = statusIt->get<std::string>();
if (status != "seeding" && status != "paused") continue;
out.push_back(SeedRecord{idIt->get<std::string>(), status == "seeding" ? SeedStatus::Seeding : SeedStatus::Paused});
}
return out;
}
std::string torrentMetaPath(const std::string& id) { return (fs::path(paths::torrentsDir()) / (id + ".torrent")).string(); }
bool torrentMetaExists(const std::string& id) {
std::error_code ec;
return fs::exists(torrentMetaPath(id), ec);
}
std::string torrentExportName(const std::string& name, const std::string& id) {
std::string base;
base.reserve(name.size());
for (unsigned char c : name) {
if (c < 0x20 || c == '<' || c == '>' || c == ':' || c == '"' || c == '/' || c == '\\' || c == '|' || c == '?' ||
c == '*') {
base += ' ';
} else {
base += static_cast<char>(c);
}
}
// Collapse runs of whitespace to a single space, then trim.
std::string collapsed;
bool lastWasSpace = false;
for (char c : base) {
bool isSpace = std::isspace(static_cast<unsigned char>(c)) != 0;
if (isSpace && lastWasSpace) continue;
collapsed += isSpace ? ' ' : c;
lastWasSpace = isSpace;
}
auto first = collapsed.find_first_not_of(' ');
auto last = collapsed.find_last_not_of(' ');
std::string trimmed = first == std::string::npos ? "" : collapsed.substr(first, last - first + 1);
// Trim trailing dots/spaces (Windows can't have filenames ending in either).
while (!trimmed.empty() && (trimmed.back() == '.' || trimmed.back() == ' ')) trimmed.pop_back();
std::string result = trimmed.empty() ? (id.empty() ? "torrent" : id) : trimmed;
if (result.size() > 180) result.resize(180);
return result + ".torrent";
}
void saveTorrentMeta(const std::string& id, const std::string& data) {
try {
fs::create_directories(paths::torrentsDir());
fs::path file = torrentMetaPath(id);
fs::path tmp = file;
tmp += ".tmp";
{
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
out.write(data.data(), static_cast<std::streamsize>(data.size()));
}
std::error_code ec;
fs::rename(tmp, file, ec);
} catch (...) {
}
}
std::optional<std::string> exportTorrentMeta(const std::string& id, const std::string& name, const std::string& dir) {
try {
fs::path source = torrentMetaPath(id);
std::error_code ec;
if (!fs::exists(source, ec)) return std::nullopt;
fs::create_directories(dir);
fs::path target = fs::path(dir) / torrentExportName(name, id);
fs::copy_file(source, target, fs::copy_options::overwrite_existing, ec);
if (ec) return std::nullopt;
return target.string();
} catch (...) {
return std::nullopt;
}
}
void deleteTorrentMeta(const std::string& id) {
std::error_code ec;
fs::remove(torrentMetaPath(id), ec);
}
} // namespace torlinkc