foxygit / Torlinkc Log in
commits tags

/src/engine/persist.cpp · 7.48 KB

raw
#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