#ifndef FDBSNAPSHOT_H #define FDBSNAPSHOT_H #include #include #include #include #include #include #include /** * Content-addressed copies of the client's cdclient.fdb, so no server maps the client's own file. * * Master copies /cdclient.fdb to resServer/cdclient-.fdb (the hash is of the copy's bytes) and makes the * matching resServer/CDServer-.sqlite from it. A small pointer file, resServer/cdclient-current, names the pair * every server should use. A new version of the fdb gets new names, so nothing is ever replaced while a process has * it open (Windows can't rename over or delete an open file; POSIX keeps the old one alive until it is closed). * * Nothing here logs or touches shared state, so it is safe on a worker thread. */ namespace FdbSnapshot { // The pointer file in resServer constexpr const char* CURRENT_FILE = "cdclient-current"; // The SQLite file the server has always used, when no copy has its own yet constexpr const char* DEFAULT_SQLITE = "CDServer.sqlite"; // 64-bit FNV-1a of the bytes uint64_t Hash(const uint8_t* data, uint64_t size, uint64_t seed = 14695981039346656037ULL); // Hash of a whole file, nullopt if it can't be read std::optional HashFile(const std::filesystem::path& path); // 16 lowercase hex digits std::string HashText(uint64_t hash); // cdclient-.fdb std::string FdbName(uint64_t hash); // CDServer-.sqlite std::string SqliteName(uint64_t hash); // The hash in a cdclient-.fdb or CDServer-.sqlite name std::optional ParseName(const std::string& name); /** * Copies source into dir as cdclient-.fdb, the hash taken of the copy so the name matches its bytes even if * source changes during the copy. The copy is written under a temporary name and renamed into place; if a copy * with that hash is already there it is kept and the temporary one removed. * * @return the hash, or nullopt with error set */ std::optional MakeCopy(const std::filesystem::path& source, const std::filesystem::path& dir, std::string& error); // The pair of files every server should use struct Current { std::string fdb; std::string sqlite; bool operator==(const Current& other) const { return fdb == other.fdb && sqlite == other.sqlite; } }; std::optional ReadCurrent(const std::filesystem::path& dir); /** * The files a server opens at startup: the pair the pointer file names, when both are there, or CDServer.sqlite * and no fdb (never the client's own file). */ struct Resolved { std::filesystem::path sqlite; std::filesystem::path fdb; // empty: read CDServer.sqlite only }; Resolved Resolve(const std::filesystem::path& dir); // Writes the pointer file under a temporary name and renames it over the old one (it is never held open) bool WriteCurrent(const std::filesystem::path& dir, const Current& current); /** * Removes every cdclient-.fdb and CDServer-.sqlite in dir whose hash is not in keep, and any leftover * temporary copies. A file that can't be removed (Windows refuses while a process still maps it) is left for the * next call. CDServer.sqlite and the client's own fdb are never touched. * * @return the names removed */ std::vector RemoveOld(const std::filesystem::path& dir, const std::set& keep); // What a poll of the client's fdb saw struct Stamp { uint64_t size = 0; int64_t mtime = 0; bool exists = false; bool operator==(const Stamp& other) const { return size == other.size && mtime == other.mtime && exists == other.exists; } bool operator!=(const Stamp& other) const { return !(*this == other); } }; Stamp StampOf(const std::filesystem::path& path); /** * Decides when a changed file is worth hashing: once its size and mtime differ from the version last taken and * have stayed the same for one poll, so a file still being written isn't copied halfway. */ class Watcher { public: // The version that is current now (taken at startup or after a reload) void Accept(const Stamp& stamp) { m_Accepted = stamp; m_Last = stamp; } // true when the file should be hashed now bool Poll(const Stamp& stamp); private: Stamp m_Accepted; Stamp m_Last; }; // Rows and a content hash per table of an fdb, to log what a new version changed struct TableSummary { uint64_t rows = 0; uint64_t hash = 0; }; std::map Summarize(const std::filesystem::path& fdb); // One line per table that was added, removed or changed ("Objects: 16012 -> 16015 rows") std::vector DescribeChanges(const std::map& before, const std::map& after); }; #endif // FDBSNAPSHOT_H