2021-09-21 01:02:23 +00:00
|
|
|
/*
|
2021-09-21 22:46:31 +00:00
|
|
|
Copyright© 2021 John Sennesael
|
|
|
|
|
2021-10-08 20:17:22 +00:00
|
|
|
UsenetSearch is Free software: you can redistribute it and/or modify
|
2021-09-21 01:02:23 +00:00
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
UsenetSearch is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with UsenetSearch. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2021-10-20 12:18:20 +00:00
|
|
|
#include "usenetsearch/Database.h"
|
|
|
|
|
|
|
|
#include "usenetsearch/Application.h"
|
2021-10-20 22:48:43 +00:00
|
|
|
#include "usenetsearch/Logger.h"
|
2021-10-20 12:18:20 +00:00
|
|
|
#include "usenetsearch/StringUtils.h"
|
|
|
|
#include "usenetsearch/ScopeExit.h"
|
|
|
|
#include "usenetsearch/Serialize.h"
|
2021-10-20 22:48:43 +00:00
|
|
|
#include "usenetsearch/UsenetClient.h"
|
2021-10-20 12:18:20 +00:00
|
|
|
|
2021-10-13 18:47:43 +00:00
|
|
|
#include <iomanip>
|
2021-09-29 23:52:54 +00:00
|
|
|
#include <chrono>
|
2021-09-21 00:48:49 +00:00
|
|
|
#include <filesystem>
|
|
|
|
#include <fstream>
|
|
|
|
#include <string>
|
2021-09-29 23:52:54 +00:00
|
|
|
#include <thread>
|
2021-09-21 00:48:49 +00:00
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
namespace usenetsearch {
|
|
|
|
|
2021-09-29 23:52:54 +00:00
|
|
|
// Database class --------------------------------------------------------------
|
|
|
|
|
2021-10-19 22:56:28 +00:00
|
|
|
void Database::CheckDbVersion(const SerializableFile& f) const
|
|
|
|
{
|
|
|
|
f.Seek(0);
|
|
|
|
const std::uint64_t ver = f.ReadInt64();
|
|
|
|
if (ver != m_databaseVersion)
|
|
|
|
{
|
2021-10-20 22:48:43 +00:00
|
|
|
Logger::Get().Fatal<DatabaseException>(
|
|
|
|
LOGID("Database"),
|
2021-10-19 22:56:28 +00:00
|
|
|
"Wrong database version - Got: " + std::to_string(ver) + " want: "
|
|
|
|
+ std::to_string(m_databaseVersion)
|
|
|
|
);
|
|
|
|
}
|
2021-09-21 00:48:49 +00:00
|
|
|
}
|
2021-09-21 22:46:31 +00:00
|
|
|
|
2021-10-20 22:48:43 +00:00
|
|
|
std::unique_ptr<NntpListEntry> Database::FindNntpEntry(std::uint64_t id)
|
|
|
|
{
|
|
|
|
const auto path = GetNewsGroupFilePath();
|
|
|
|
if (!std::filesystem::exists(path)) return nullptr;
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
CheckDbVersion(io);
|
|
|
|
const std::uint64_t numGroups = io.ReadInt64();
|
|
|
|
std::unique_ptr<NntpListEntry> result = nullptr;
|
|
|
|
for (std::uint64_t n = 0; n != numGroups; ++n)
|
|
|
|
{
|
|
|
|
NntpListEntry entry;
|
|
|
|
io >> entry;
|
|
|
|
if (entry.id == id)
|
|
|
|
{
|
|
|
|
result = std::make_unique<NntpListEntry>(entry);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-10-19 01:19:11 +00:00
|
|
|
std::unique_ptr<NntpListEntry> Database::FindNntpEntry(
|
|
|
|
const std::string& subject)
|
2021-09-29 23:52:54 +00:00
|
|
|
{
|
2021-10-19 22:56:28 +00:00
|
|
|
const auto path = GetNewsGroupFilePath();
|
|
|
|
if (!std::filesystem::exists(path)) return nullptr;
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
CheckDbVersion(io);
|
|
|
|
const std::uint64_t numGroups = io.ReadInt64();
|
2021-10-19 01:19:11 +00:00
|
|
|
std::unique_ptr<NntpListEntry> result = nullptr;
|
|
|
|
for (std::uint64_t n = 0; n != numGroups; ++n)
|
|
|
|
{
|
|
|
|
NntpListEntry entry;
|
2021-10-19 22:56:28 +00:00
|
|
|
io >> entry;
|
2021-10-19 01:19:11 +00:00
|
|
|
if (entry.name == subject)
|
|
|
|
{
|
|
|
|
result = std::make_unique<NntpListEntry>(entry);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::uint32_t Database::GetLastIndexedArticle(std::uint64_t newsgroupID)
|
|
|
|
{
|
2021-10-19 22:56:28 +00:00
|
|
|
const auto path = GetNewsGroupFilePath();
|
|
|
|
if (!std::filesystem::exists(path))
|
|
|
|
{
|
2021-10-20 22:48:43 +00:00
|
|
|
Logger::Get().Fatal<DatabaseException>(
|
|
|
|
LOGID("Database"),
|
|
|
|
"No indexed articles for newsgroup: "
|
|
|
|
+ std::to_string(newsgroupID)
|
|
|
|
);
|
2021-10-19 22:56:28 +00:00
|
|
|
}
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
CheckDbVersion(io);
|
|
|
|
const std::uint64_t numGroups = io.ReadInt64();
|
2021-10-19 01:19:11 +00:00
|
|
|
for (std::uint64_t n = 0; n != numGroups; ++n)
|
|
|
|
{
|
|
|
|
NntpListEntry entry;
|
2021-10-19 22:56:28 +00:00
|
|
|
io >> entry;
|
2021-10-19 01:19:11 +00:00
|
|
|
if (entry.id == newsgroupID)
|
|
|
|
{
|
|
|
|
return entry.lastIndexedArticle;
|
|
|
|
}
|
|
|
|
}
|
2021-10-20 22:48:43 +00:00
|
|
|
Logger::Get().Fatal<DatabaseException>(
|
|
|
|
LOGID("Database"),
|
|
|
|
"No indexed articles for newsgroup: " + std::to_string(newsgroupID)
|
|
|
|
);
|
|
|
|
return NntpListEntry::NOT_INDEXED;
|
2021-09-29 23:52:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::filesystem::path Database::GetTokenFilePath(
|
|
|
|
const std::string& token,
|
|
|
|
bool mkdirs
|
|
|
|
)
|
|
|
|
{
|
|
|
|
const std::string md5 = StringHash(token);
|
|
|
|
const std::string tok1 = md5.substr(0, 2);
|
2021-10-08 20:17:22 +00:00
|
|
|
const std::string tok2 = md5.substr(2, 1);
|
|
|
|
const std::string tok3 = md5.substr(3, 2);
|
|
|
|
const std::filesystem::path groupPath = m_databasePath / "tokens" / tok1
|
|
|
|
/ tok2;
|
2021-09-29 23:52:54 +00:00
|
|
|
if (mkdirs)
|
|
|
|
{
|
|
|
|
if (!std::filesystem::exists(groupPath))
|
|
|
|
{
|
|
|
|
std::filesystem::create_directories(groupPath);
|
|
|
|
}
|
|
|
|
}
|
2021-10-08 20:17:22 +00:00
|
|
|
const auto groupFile = tok3 + ".db";
|
2021-09-29 23:52:54 +00:00
|
|
|
return groupPath / groupFile;
|
|
|
|
}
|
|
|
|
|
2021-10-19 01:19:11 +00:00
|
|
|
std::uint64_t Database::GetUniqueNntpEntryId(
|
|
|
|
const std::vector<NntpListEntry>& list) const
|
2021-09-29 23:52:54 +00:00
|
|
|
{
|
2021-10-19 01:19:11 +00:00
|
|
|
std::uint64_t result{0};
|
|
|
|
for (auto& entry: list)
|
|
|
|
{
|
|
|
|
if (result <= entry.id)
|
|
|
|
{
|
|
|
|
result = entry.id + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
2021-09-21 22:46:31 +00:00
|
|
|
}
|
|
|
|
|
2021-10-19 01:19:11 +00:00
|
|
|
void Database::MaxTreeDepth(std::uint8_t depth)
|
2021-09-21 22:46:31 +00:00
|
|
|
{
|
2021-10-19 01:19:11 +00:00
|
|
|
m_maxTreeDepth = depth;
|
2021-09-21 22:46:31 +00:00
|
|
|
}
|
|
|
|
|
2021-09-21 01:37:11 +00:00
|
|
|
std::unique_ptr<std::vector<NntpListEntry>> Database::LoadNewsgroupList()
|
|
|
|
{
|
|
|
|
auto result = std::make_unique<std::vector<NntpListEntry>>();
|
2021-10-19 22:56:28 +00:00
|
|
|
const auto path = GetNewsGroupFilePath();
|
|
|
|
if (!std::filesystem::exists(path)) return result;
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
CheckDbVersion(io);
|
|
|
|
const size_t newsGroupCount = io.ReadInt64();
|
|
|
|
|
2021-09-21 01:37:11 +00:00
|
|
|
for (size_t numLoaded = 0; numLoaded != newsGroupCount; ++numLoaded)
|
|
|
|
{
|
|
|
|
NntpListEntry entry;
|
2021-10-19 22:56:28 +00:00
|
|
|
io >> entry;
|
2021-09-21 01:37:11 +00:00
|
|
|
result->emplace_back(entry);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2021-09-21 00:48:49 +00:00
|
|
|
|
|
|
|
void Database::Open(std::filesystem::path dbPath)
|
|
|
|
{
|
|
|
|
m_databasePath = dbPath;
|
|
|
|
if (!std::filesystem::exists(dbPath))
|
|
|
|
{
|
|
|
|
std::filesystem::create_directory(dbPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-19 22:56:28 +00:00
|
|
|
std::filesystem::path Database::GetNewsGroupFilePath() const
|
2021-09-21 00:48:49 +00:00
|
|
|
{
|
2021-10-19 22:56:28 +00:00
|
|
|
return m_databasePath / "newsgroups.db";
|
2021-10-08 20:17:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Database::ParseTokenFile(
|
|
|
|
const std::filesystem::path& dbFile,
|
|
|
|
std::function<void(const ArticleEntry& entry)> onParse)
|
|
|
|
{
|
|
|
|
if (!std::filesystem::exists(dbFile))
|
|
|
|
{
|
2021-10-20 22:48:43 +00:00
|
|
|
Logger::Get().Fatal<DatabaseException>(
|
|
|
|
LOGID("Database"),
|
2021-10-08 20:17:22 +00:00
|
|
|
"File does not exist: " + dbFile.string()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(dbFile);
|
|
|
|
const std::uint64_t tokenCount = io.ReadInt64();
|
|
|
|
for (std::uint64_t i = 0; i != tokenCount; ++i)
|
2021-09-21 00:48:49 +00:00
|
|
|
{
|
2021-10-20 22:48:43 +00:00
|
|
|
if (Application::Get().ShouldStop()) return;
|
2021-10-08 20:17:22 +00:00
|
|
|
ArticleEntry token;
|
|
|
|
io >> token;
|
|
|
|
onParse(token);
|
2021-09-21 00:48:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-19 01:19:11 +00:00
|
|
|
void Database::SetLastIndexedArticle(
|
|
|
|
std::uint64_t newsgroupID,
|
|
|
|
std::int32_t articleID)
|
2021-09-21 00:48:49 +00:00
|
|
|
{
|
2021-10-19 01:19:11 +00:00
|
|
|
auto outItems = LoadNewsgroupList();
|
|
|
|
bool found{false};
|
|
|
|
if (outItems)
|
2021-09-21 00:48:49 +00:00
|
|
|
{
|
2021-10-19 01:19:11 +00:00
|
|
|
for (auto& entry: *outItems)
|
|
|
|
{
|
|
|
|
if (entry.id == newsgroupID)
|
|
|
|
{
|
|
|
|
entry.lastIndexedArticle = articleID;
|
|
|
|
found = true;
|
2021-10-19 22:56:28 +00:00
|
|
|
break;
|
2021-10-19 01:19:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found)
|
|
|
|
{
|
2021-10-20 22:48:43 +00:00
|
|
|
Logger::Get().Fatal<DatabaseException>(
|
|
|
|
LOGID("Database"),
|
2021-10-19 01:19:11 +00:00
|
|
|
"Attempt to update newsgroup not found in database - id: "
|
2021-10-20 22:48:43 +00:00
|
|
|
+ std::to_string(newsgroupID)
|
|
|
|
);
|
2021-09-29 23:52:54 +00:00
|
|
|
}
|
2021-10-19 01:19:11 +00:00
|
|
|
UpdateNewsgroupList(*outItems);
|
2021-09-29 23:52:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Database::SaveSearchTokens(
|
2021-10-08 20:17:22 +00:00
|
|
|
std::uint64_t newsgroupID,
|
2021-09-29 23:52:54 +00:00
|
|
|
std::uint64_t articleID,
|
|
|
|
const std::string& searchString)
|
|
|
|
{
|
|
|
|
const std::string sstr(searchString);
|
|
|
|
StringTreeOperation(
|
|
|
|
sstr,
|
|
|
|
" ",
|
|
|
|
m_maxTreeDepth,
|
|
|
|
[&](const std::string& subToken, const std::string& str){
|
2021-10-20 22:48:43 +00:00
|
|
|
const std::string tok = Application::Get().GetFilter().ProcessToken(
|
2021-10-19 01:19:11 +00:00
|
|
|
subToken,
|
|
|
|
str
|
|
|
|
);
|
2021-10-12 23:41:03 +00:00
|
|
|
if (tok.empty()) return;
|
|
|
|
SaveToken(tok, newsgroupID, articleID);
|
2021-09-29 23:52:54 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-10-08 20:17:22 +00:00
|
|
|
bool Database::HasToken(
|
2021-09-29 23:52:54 +00:00
|
|
|
const std::string& subtoken,
|
2021-10-08 20:17:22 +00:00
|
|
|
std::uint64_t newsgroupID,
|
|
|
|
std::uint32_t articleID)
|
2021-09-29 23:52:54 +00:00
|
|
|
{
|
2021-10-08 20:17:22 +00:00
|
|
|
if (subtoken.empty()) return false;
|
|
|
|
const std::filesystem::path path = GetTokenFilePath(subtoken, true);
|
|
|
|
if (!std::filesystem::exists(path)) return false;
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
const std::uint64_t tokenCount = io.ReadInt64();
|
|
|
|
for (std::uint64_t i = 0; i != tokenCount; ++i)
|
2021-09-29 23:52:54 +00:00
|
|
|
{
|
2021-10-08 20:17:22 +00:00
|
|
|
ArticleEntry token;
|
|
|
|
io >> token;
|
|
|
|
if (token.newsgroupID == newsgroupID)
|
|
|
|
{
|
|
|
|
if (token.articleID == articleID) return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-10-12 23:41:03 +00:00
|
|
|
std::unique_ptr<std::vector<ArticleEntry>> Database::LoadTokens(
|
|
|
|
const std::filesystem::path dbFile,
|
|
|
|
const std::string& subtoken)
|
|
|
|
{
|
|
|
|
auto result = std::make_unique<std::vector<ArticleEntry>>();
|
|
|
|
if (!std::filesystem::exists(dbFile)) return result;
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(dbFile);
|
|
|
|
const std::uint64_t tokenCount = io.ReadInt64();
|
|
|
|
const auto tokenHash = StringHashBytes(subtoken);
|
|
|
|
for (std::uint64_t ntok = 0; ntok != tokenCount; ++ntok)
|
|
|
|
{
|
|
|
|
ArticleEntry entry{};
|
|
|
|
io >> entry;
|
|
|
|
if (entry.hash != tokenHash) continue;
|
|
|
|
result->emplace_back(entry);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-10-08 20:17:22 +00:00
|
|
|
void Database::SaveToken(
|
|
|
|
const std::string& subtoken,
|
|
|
|
std::uint64_t newsgroupID,
|
|
|
|
std::uint32_t articleID)
|
|
|
|
{
|
|
|
|
if (subtoken.empty()) return;
|
|
|
|
const std::filesystem::path path = GetTokenFilePath(subtoken, true);
|
|
|
|
const bool exists = std::filesystem::exists(path);
|
|
|
|
|
|
|
|
SerializableFile io;
|
|
|
|
io.Open(path);
|
|
|
|
ArticleEntry token{};
|
|
|
|
token.articleID = articleID;
|
|
|
|
token.newsgroupID = newsgroupID;
|
|
|
|
token.hash = StringHashBytes(subtoken);
|
|
|
|
if (exists)
|
|
|
|
{
|
|
|
|
// Read token count and increment it by one, and write it back.
|
|
|
|
const std::uint64_t tokenCount = io.ReadInt64() + 1;
|
|
|
|
io.Seek(0, std::ios_base::beg);
|
|
|
|
io << tokenCount;
|
|
|
|
// Now seek back to the end of the file to append our token entry.
|
|
|
|
io.Seek(0, std::ios_base::end);
|
2021-09-29 23:52:54 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2021-10-08 20:17:22 +00:00
|
|
|
// A new file just has a token count of 1 - should already be at pos=0.
|
|
|
|
io << std::uint64_t{1};
|
2021-09-21 00:48:49 +00:00
|
|
|
}
|
2021-10-08 20:17:22 +00:00
|
|
|
// write out token.
|
|
|
|
io << token;
|
2021-09-21 00:48:49 +00:00
|
|
|
}
|
|
|
|
|
2021-10-12 23:41:03 +00:00
|
|
|
std::unique_ptr<std::vector<ArticleEntry>> Database::Search(
|
|
|
|
const std::string& searchString)
|
|
|
|
{
|
|
|
|
auto result = std::make_unique<std::vector<ArticleEntry>>();
|
|
|
|
// Tokenize the search string.
|
|
|
|
std::vector<std::string> searchTokens;
|
|
|
|
StringTreeOperation(
|
|
|
|
searchString,
|
|
|
|
" ",
|
|
|
|
m_maxTreeDepth,
|
|
|
|
[&searchTokens](const std::string& subToken, const std::string&){
|
|
|
|
searchTokens.emplace_back(subToken);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
for (const auto& searchToken: searchTokens)
|
|
|
|
{
|
|
|
|
const auto path = GetTokenFilePath(searchToken, false);
|
|
|
|
const bool exists = std::filesystem::exists(path);
|
|
|
|
if (!exists) continue;
|
|
|
|
const auto foundTokens = LoadTokens(path, searchToken);
|
|
|
|
if (foundTokens->empty()) continue;
|
|
|
|
result->insert(result->end(), foundTokens->begin(), foundTokens->end());
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-10-19 22:56:28 +00:00
|
|
|
void Database::SyncLastUpdated(
|
|
|
|
NntpListEntry& a,
|
|
|
|
NntpListEntry& b
|
|
|
|
)
|
|
|
|
{
|
|
|
|
// If both are equal, nothing to do here.
|
|
|
|
if (a.lastIndexedArticle == b.lastIndexedArticle) return;
|
|
|
|
// Whichever one's not indexed, gets the value of the other.
|
|
|
|
if (a.lastIndexedArticle == NntpListEntry::NOT_INDEXED)
|
|
|
|
{
|
|
|
|
a.lastIndexedArticle = b.lastIndexedArticle;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (b.lastIndexedArticle == NntpListEntry::NOT_INDEXED)
|
|
|
|
{
|
|
|
|
b.lastIndexedArticle = a.lastIndexedArticle;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Otherwise, whichever's higher wins.
|
|
|
|
if (a.lastIndexedArticle > b.lastIndexedArticle)
|
|
|
|
{
|
|
|
|
b.lastIndexedArticle = a.lastIndexedArticle;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
a.lastIndexedArticle = b.lastIndexedArticle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-19 01:19:11 +00:00
|
|
|
void Database::UpdateNewsgroupList(std::vector<NntpListEntry>& list)
|
|
|
|
{
|
|
|
|
if (list.size() == 0) return;
|
|
|
|
|
|
|
|
auto outList = LoadNewsgroupList();
|
|
|
|
for (auto& entry: list)
|
|
|
|
{
|
|
|
|
bool found{false};
|
|
|
|
if (outList)
|
|
|
|
{
|
|
|
|
std::for_each(
|
|
|
|
outList->begin(),
|
|
|
|
outList->end(),
|
2021-10-19 22:56:28 +00:00
|
|
|
[this, &entry, &found](NntpListEntry& oldEntry)
|
2021-10-19 01:19:11 +00:00
|
|
|
{
|
|
|
|
if (oldEntry.name == entry.name)
|
|
|
|
{
|
2021-10-19 22:56:28 +00:00
|
|
|
// update existing- the passed entry has updated counts
|
|
|
|
// and status.
|
2021-10-19 01:19:11 +00:00
|
|
|
found = true;
|
|
|
|
oldEntry.count = entry.count;
|
|
|
|
oldEntry.high = entry.high;
|
|
|
|
oldEntry.low = entry.low;
|
|
|
|
oldEntry.status = entry.status;
|
2021-10-19 22:56:28 +00:00
|
|
|
// As for lastIndexed: whoemever's higher wins.
|
|
|
|
SyncLastUpdated(entry, oldEntry);
|
|
|
|
// The ID should be sticky - whatever's already in the
|
|
|
|
// db is what we care about, so update it in the passed
|
|
|
|
// entries.
|
|
|
|
entry.id = oldEntry.id;
|
2021-10-19 01:19:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (found) continue;
|
|
|
|
// add new.
|
2021-10-19 22:56:28 +00:00
|
|
|
NntpListEntry newEntry(entry);
|
2021-10-19 01:19:11 +00:00
|
|
|
newEntry.id = GetUniqueNntpEntryId(*outList);
|
|
|
|
outList->emplace_back(newEntry);
|
|
|
|
entry.id = newEntry.id;
|
|
|
|
}
|
2021-10-19 22:56:28 +00:00
|
|
|
SerializableFile io;
|
|
|
|
io.Open(GetNewsGroupFilePath());
|
|
|
|
io << m_databaseVersion;
|
|
|
|
io << std::uint64_t{outList->size()};
|
2021-10-19 01:19:11 +00:00
|
|
|
std::for_each(
|
|
|
|
outList->begin(),
|
|
|
|
outList->end(),
|
|
|
|
[&](const NntpListEntry& e)
|
|
|
|
{
|
2021-10-19 22:56:28 +00:00
|
|
|
io << e;
|
2021-10-19 01:19:11 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-21 00:48:49 +00:00
|
|
|
} // namespace usenetsearch
|