code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "utils.h"

#include <boost/asio/ip/host_name.hpp>
#include <boost/tokenizer.hpp>
#include <boost/regex.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/nowide/convert.hpp>
#include <spdlog/spdlog.h>
#include "utils/log.h"
#include "utils/location.h"

#define TOML_EXCEPTIONS 0
#include <toml++/toml.h>

namespace sys = boost::system;

#if defined(__unix__)
static const std::string home_path = "~/.config/syncspirit";
#else
static const std::string home_path = "~/syncspirit";
#endif

namespace syncspirit::config {

using level_t = spdlog::level::level_enum;

using home_option_t = outcome::result<bfs::path>;

static std::string get_device_name() noexcept {
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
    wchar_t device_name[MAX_COMPUTERNAME_LENGTH + 1] = {0};
    DWORD device_name_sz = sizeof(device_name) / sizeof(wchar_t);
    if (GetComputerNameW(device_name, &device_name_sz)) {
        return boost::nowide::narrow(device_name, static_cast<size_t>(device_name_sz));
#else
    sys::error_code ec;
    auto device_name = boost::asio::ip::host_name(ec);
    if (!ec) {
        return device_name;
#endif
    } else {
        return "localhost";
    }
}

config_result_t get_config(std::istream &config, const bfs::path &config_path) {
    main_t cfg;
    cfg.config_path = config_path;

    auto home_opt = utils::get_home_dir();
    auto r = toml::parse(config);
    if (!r) {
        return std::string(r.error().description());
    }

    auto &root_tbl = r.table();
    // main
    {
        auto t = root_tbl["main"];
        auto &c = cfg;
        auto timeout = t["timeout"].value<std::uint32_t>();
        if (!timeout) {
            return "main/timeout is incorrect or missing";
        }
        c.timeout = timeout.value();

        auto device_name = t["device_name"].value<std::string>();
        if (!device_name) {
            device_name = get_device_name();
        }
        c.device_name = device_name.value();

        auto default_location = t["default_location"].value<std::string>();
        if (!default_location) {
            return "main/default_location is incorrect or missing";
        }
        // otherwise it will be corrupted somehow
        c.default_location = boost::nowide::widen(default_location.value());

        auto hasher_threads = t["hasher_threads"].value<std::uint32_t>();
        if (!hasher_threads) {
            return "main/hasher_threads is incorrect or missing";
        }
        c.hasher_threads = hasher_threads.value();
    };

    // log
    {
        auto t = root_tbl["log"];
        auto &c = cfg.log_configs;
        if (t.is_array_of_tables()) {
            auto arr = t.as_array();
            for (size_t i = 0; i < arr->size(); ++i) {
                auto node = arr->get(i);
                if (node->is_table()) {
                    auto t = *node->as_table();
                    auto level = t["level"].value<std::string>();
                    if (!level) {
                        return "log/level is incorrect or missing (" + std::to_string(i + 1) + ")";
                    }
                    auto name = t["name"].value<std::string>();
                    if (!name) {
                        return "log/name is incorrect or missing (" + std::to_string(i + 1) + ")";
                    }
                    log_config_t log_config;
                    log_config.level = utils::get_log_level(level.value()).value_or(level_t::debug);
                    log_config.name = name.value();

                    auto sinks = t["sinks"];
                    if (sinks) {
                        auto s_arr = sinks.as_array();
                        for (size_t j = 0; j < s_arr->size(); ++j) {
                            auto sink = s_arr->get(j)->value<std::string>();
                            if (!sink) {
                                return "log/sinks " + std::to_string(j + 1) + " is incorrect or missing (" +
                                       std::to_string(i + 1) + ")";
                            }
                            log_config.sinks.emplace_back(sink.value());
                        }
                    }
                    c.emplace_back(std::move(log_config));
                }
            }
        }
    }

    // local_discovery
    {
        auto t = root_tbl["local_discovery"];
        auto &c = cfg.local_announce_config;

        auto enabled = t["enabled"].value<bool>();
        if (!enabled) {
            return "local_discovery/enabled is incorrect or missing";
        }
        c.enabled = enabled.value();

        auto port = t["port"].value<std::uint16_t>();
        if (!port) {
            return "local_discovery/port is incorrect or missing";
        }
        c.port = port.value();

        auto frequency = t["frequency"].value<std::uint32_t>();
        if (!frequency) {
            return "local_discovery/frequency is incorrect or missing";
        }
        c.frequency = frequency.value();
    }

    // global_discovery
    {
        auto t = root_tbl["global_discovery"];
        auto &c = cfg.global_announce_config;

        auto enabled = t["enabled"].value<bool>();
        if (!enabled) {
            return "global_discovery/enabled is incorrect or missing";
        }
        c.enabled = enabled.value();

        auto debug = t["debug"].value<bool>();
        if (!debug) {
            return "global_discovery/debug is incorrect or missing";
        }
        c.debug = debug.value();

        auto url = t["announce_url"].value<std::string>();
        if (!url) {
            return "global_discovery/announce_url is incorrect or missing";
        }
        auto announce_url = utils::parse(url.value().c_str());
        if (!announce_url) {
            return "global_discovery/announce_url is not url";
        }
        c.announce_url = announce_url;

        auto device_id = t["device_id"].value<std::string>();
        if (!device_id) {
            return "global_discovery/device_id is incorrect or missing";
        }
        c.device_id = device_id.value();

        auto cert_file = t["cert_file"].value<std::string>();
        if (!cert_file) {
            return "global_discovery/cert_file is incorrect or missing";
        }
        c.cert_file = utils::expand_home(cert_file.value(), home_opt);

        auto key_file = t["key_file"].value<std::string>();
        if (!key_file) {
            return "global_discovery/key_file is incorrect or missing";
        }
        c.key_file = utils::expand_home(key_file.value(), home_opt);

        auto rx_buff_size = t["rx_buff_size"].value<std::uint32_t>();
        if (!rx_buff_size) {
            return "global_discovery/rx_buff_size is incorrect or missing";
        }
        c.rx_buff_size = rx_buff_size.value();

        auto timeout = t["timeout"].value<std::uint32_t>();
        if (!timeout) {
            return "global_discovery/timeout is incorrect or missing";
        }
        c.timeout = timeout.value();
    };

    // upnp
    {
        auto t = root_tbl["upnp"];
        auto &c = cfg.upnp_config;

        auto enabled = t["enabled"].value<bool>();
        if (!enabled) {
            return "upnp/enabled is incorrect or missing";
        }
        c.enabled = enabled.value();

        auto debug = t["debug"].value<bool>();
        if (!debug) {
            return "upnp/debug is incorrect or missing";
        }
        c.debug = debug.value();

        auto max_wait = t["max_wait"].value<std::uint32_t>();
        if (!max_wait) {
            return "upnp/max_wait is incorrect or missing";
        }
        c.max_wait = max_wait.value();

        auto external_port = t["external_port"].value<std::uint32_t>();
        if (!external_port) {
            return "upnp/external_port is incorrect or missing";
        }
        c.external_port = external_port.value();

        auto rx_buff_size = t["rx_buff_size"].value<std::uint32_t>();
        if (!rx_buff_size) {
            return "upnp/rx_buff_size is incorrect or missing";
        }
        c.rx_buff_size = rx_buff_size.value();
    };

    // relay
    {
        auto t = root_tbl["relay"];
        auto &c = cfg.relay_config;

        auto enabled = t["enabled"].value<bool>();
        if (!enabled) {
            return "relay/enabled is incorrect or missing";
        }
        c.enabled = enabled.value();

        auto debug = t["debug"].value<bool>();
        if (!debug) {
            return "relay/debug is incorrect or missing";
        }
        c.debug = debug.value();

        auto discovery_url_str = t["discovery_url"].value<std::string>();
        if (!discovery_url_str) {
            return "upnp/discovery_url is incorrect or missing";
        }
        auto discovery_url = utils::parse(discovery_url_str.value());
        if (!discovery_url_str) {
            return "upnp/discovery_url is non a valid url";
        }
        c.discovery_url = discovery_url;

        auto rx_buff_size = t["rx_buff_size"].value<std::uint32_t>();
        if (!rx_buff_size) {
            return "relay/rx_buff_size is incorrect or missing";
        }
        c.rx_buff_size = rx_buff_size.value();
    };

    // bep
    {
        auto t = root_tbl["bep"];
        auto &c = cfg.bep_config;
        auto rx_buff_size = t["rx_buff_size"].value<std::uint32_t>();
        if (!rx_buff_size) {
            return "bep/rx_buff_size is incorrect or missing";
        }
        c.rx_buff_size = rx_buff_size.value();
        auto tx_buff_limit = t["tx_buff_limit"].value<std::uint32_t>();
        if (!tx_buff_limit) {
            return "bep/tx_buff_limit is incorrect or missing";
        }
        c.tx_buff_limit = tx_buff_limit.value();

        auto connect_timeout = t["connect_timeout"].value<std::uint32_t>();
        if (!connect_timeout) {
            return "bep/connect_timeout is incorrect or missing";
        }
        c.connect_timeout = connect_timeout.value();

        auto request_timeout = t["request_timeout"].value<std::uint32_t>();
        if (!request_timeout) {
            return "bep/request_timeout is incorrect or missing";
        }
        c.request_timeout = request_timeout.value();

        auto tx_timeout = t["tx_timeout"].value<std::uint32_t>();
        if (!tx_timeout) {
            return "bep/tx_timeout is incorrect or missing";
        }
        c.tx_timeout = tx_timeout.value();

        auto rx_timeout = t["rx_timeout"].value<std::uint32_t>();
        if (!rx_timeout) {
            return "bep/rx_timeout is incorrect or missing";
        }
        c.rx_timeout = rx_timeout.value();

        auto blocks_max_requested = t["blocks_max_requested"].value<std::uint32_t>();
        if (!blocks_max_requested) {
            return "bep/blocks_max_requested is incorrect or missing";
        }
        c.blocks_max_requested = blocks_max_requested.value();

        auto blocks_simultaneous_write = t["blocks_simultaneous_write"].value<std::uint32_t>();
        if (!blocks_simultaneous_write) {
            return "bep/blocks_simultaneous_write is incorrect or missing";
        }
        c.blocks_simultaneous_write = blocks_simultaneous_write.value();

        auto advances_per_iteration = t["advances_per_iteration"].value<std::uint32_t>();
        if (!advances_per_iteration) {
            return "bep/advances_per_iteration is incorrect or missing";
        }
        c.advances_per_iteration = advances_per_iteration.value();

        auto stats_interval = t["stats_interval"].value<std::uint32_t>();
        if (!stats_interval) {
            return "bep/stats_interval is incorrect or missing";
        }
        c.stats_interval = stats_interval.value();
    }

    // dialer
    {
        auto t = root_tbl["dialer"];
        auto &c = cfg.dialer_config;

        auto enabled = t["enabled"].value<bool>();
        if (!enabled) {
            return "dialer/enabled is incorrect or missing";
        }
        c.enabled = enabled.value();

        auto redial_timeout = t["redial_timeout"].value<std::uint32_t>();
        if (!redial_timeout) {
            return "dialer/redial_timeout is incorrect or missing";
        }
        c.redial_timeout = redial_timeout.value();

        auto skip_discovers = t["skip_discovers"].value<std::uint32_t>();
        if (!skip_discovers) {
            return "dialer/skip_discovers is incorrect or missing";
        }
        c.skip_discovers = skip_discovers.value();
    }

    // fs
    {
        auto t = root_tbl["fs"];
        auto &c = cfg.fs_config;

        auto temporally_timeout = t["temporally_timeout"].value<std::uint32_t>();
        if (!temporally_timeout) {
            return "fs/temporally_timeout is incorrect or missing";
        }
        c.temporally_timeout = temporally_timeout.value();

        auto mru_size = t["mru_size"].value<std::uint32_t>();
        if (!mru_size) {
            return "fs/mru_size is incorrect or missing";
        }
        c.mru_size = mru_size.value();

        auto bytes_scan_iteration_limit = t["bytes_scan_iteration_limit"].value<std::int64_t>();
        if (!bytes_scan_iteration_limit) {
            return "fs/bytes_scan_iteration_limit is incorrect or missing";
        }
        if (bytes_scan_iteration_limit.value() <= 0) {
            return "fs/bytes_scan_iteration_limit should be >= 0";
        }
        c.bytes_scan_iteration_limit = bytes_scan_iteration_limit.value();

        auto files_scan_iteration_limit = t["files_scan_iteration_limit"].value<std::int64_t>();
        if (!files_scan_iteration_limit) {
            return "fs/files_scan_iteration_limit is incorrect or missing";
        }
        if (files_scan_iteration_limit.value() <= 0) {
            return "fs/files_scan_iteration_limit should be >= 0";
        }
        c.files_scan_iteration_limit = files_scan_iteration_limit.value();
    }

    // db
    {
        auto t = root_tbl["db"];
        auto &c = cfg.db_config;

        auto upper_limit = t["upper_limit"].value<std::int64_t>();
        if (!upper_limit) {
            return "db/upper_limit is incorrect or missing";
        }
        c.upper_limit = upper_limit.value();

        auto uncommitted_threshold = t["uncommitted_threshold"].value<std::uint32_t>();
        if (!uncommitted_threshold) {
            return "db/uncommitted_threshold is incorrect or missing";
        }
        c.uncommitted_threshold = uncommitted_threshold.value();

        auto max_blocks_per_diff = t["max_blocks_per_diff"].value<std::uint32_t>();
        if (!max_blocks_per_diff) {
            return "db/max_blocks_per_diff is incorrect or missing";
        }
        if (max_blocks_per_diff.value() <= 10) {
            return "db/max_blocks_per_diff should be > 10";
        }
        c.max_blocks_per_diff = max_blocks_per_diff.value();

        auto max_files_per_diff = t["max_files_per_diff"].value<std::uint32_t>();
        if (!max_files_per_diff) {
            return "db/max_files_per_diff is incorrect or missing";
        }
        if (max_files_per_diff.value() <= 10) {
            return "db/max_files_per_diff should be > 10";
        }
        c.max_files_per_diff = max_files_per_diff.value();
    }

    // fltk
    {
        auto t = root_tbl["fltk"];
        auto &c = cfg.fltk_config;

        auto level = t["level"].value<std::string>();
        if (!level) {
            return "fltk/level is incorrect or missing";
        }
        c.level = utils::get_log_level(level.value()).value_or(level_t::debug);

        auto display_deleted = t["display_deleted"].value<bool>();
        if (!display_deleted) {
            return "fltk/display_deleted is incorrect or missing";
        }
        c.display_deleted = display_deleted.value();

        auto display_missing = t["display_missing"].value<bool>();
        if (!display_missing) {
            return "fltk/display_missing is incorrect or missing";
        }
        c.display_missing = display_missing.value();

        auto display_colorized = t["display_colorized"].value<bool>();
        if (!display_colorized) {
            return "fltk/display_colorized is incorrect or missing";
        }
        c.display_colorized = display_colorized.value();

        auto main_window_width = t["main_window_width"].value<std::int64_t>();
        if (!main_window_width) {
            return "fltk/main_window_width is incorrect or missing";
        }
        c.main_window_width = main_window_width.value();

        auto main_window_height = t["main_window_height"].value<std::int64_t>();
        if (!main_window_height) {
            return "fltk/main_window_height is incorrect or missing";
        }
        c.main_window_height = main_window_height.value();

        auto left_panel_share = t["left_panel_share"].value<double>();
        if (!left_panel_share) {
            return "fltk/left_panel_share is incorrect or missing";
        }
        c.left_panel_share = left_panel_share.value();

        auto bottom_panel_share = t["bottom_panel_share"].value<double>();
        if (!bottom_panel_share) {
            return "fltk/bottom_panel_share is incorrect or missing";
        }
        c.bottom_panel_share = bottom_panel_share.value();

        auto log_records_buffer = t["log_records_buffer"].value<std::uint32_t>();
        if (!log_records_buffer) {
            return "fltk/log_records_buffer is incorrect or missing";
        }
        if (log_records_buffer.value() < 100) {
            return "fltk/log_records_buffer should be >= 0";
        }
        c.log_records_buffer = log_records_buffer.value();
    }

    return cfg;
}

static std::string_view get_level(spdlog::level::level_enum level) noexcept {
    using L = spdlog::level::level_enum;
    switch (level) {
    case L::critical:
        return "critical";
    case L::debug:
        return "debug";
    case L::err:
        return "error";
    case L::info:
        return "info";
    case L::trace:
        return "trace";
    case L::warn:
        return "warn";
    case L::off:
        return "off";
    case L::n_levels:
        return "off";
    }
    return "unknown";
}

outcome::result<void> serialize(const main_t cfg, std::ostream &out) noexcept {
    auto logs = toml::array{};
    for (auto &c : cfg.log_configs) {
        auto sinks = toml::array{};
        for (auto &sink : c.sinks) {
            sinks.emplace_back<std::string>(sink);
        }
        auto log_table = toml::table{{
            {"name", c.name},
            {"level", get_level(c.level)},
            {"sinks", sinks},
        }};
        logs.push_back(log_table);
    }

    auto tbl = toml::table{{
        {"main", toml::table{{
                     {"hasher_threads", cfg.hasher_threads},
                     {"timeout", cfg.timeout},
                     {"device_name", cfg.device_name},
                     {"default_location", cfg.default_location.string()},
                 }}},
        {"log", logs},
        {"local_discovery", toml::table{{
                                {"enabled", cfg.local_announce_config.enabled},
                                {"port", cfg.local_announce_config.port},
                                {"frequency", cfg.local_announce_config.frequency},
                            }}},
        {"global_discovery", toml::table{{
                                 {"enabled", cfg.global_announce_config.enabled},
                                 {"debug", cfg.global_announce_config.debug},
                                 {"announce_url", cfg.global_announce_config.announce_url->buffer().data()},
                                 {"device_id", cfg.global_announce_config.device_id},
                                 {"cert_file", cfg.global_announce_config.cert_file},
                                 {"key_file", cfg.global_announce_config.key_file},
                                 {"rx_buff_size", cfg.global_announce_config.rx_buff_size},
                                 {"timeout", cfg.global_announce_config.timeout},
                             }}},
        {"upnp", toml::table{{
                     {"enabled", cfg.upnp_config.enabled},
                     {"debug", cfg.upnp_config.debug},
                     {"max_wait", cfg.upnp_config.max_wait},
                     {"external_port", cfg.upnp_config.external_port},
                     {"rx_buff_size", cfg.upnp_config.rx_buff_size},
                 }}},
        {"bep", toml::table{{
                    {"advances_per_iteration", cfg.bep_config.advances_per_iteration},
                    {"blocks_max_requested", cfg.bep_config.blocks_max_requested},
                    {"blocks_simultaneous_write", cfg.bep_config.blocks_simultaneous_write},
                    {"connect_timeout", cfg.bep_config.connect_timeout},
                    {"request_timeout", cfg.bep_config.request_timeout},
                    {"rx_buff_size", cfg.bep_config.rx_buff_size},
                    {"rx_timeout", cfg.bep_config.rx_timeout},
                    {"stats_interval", cfg.bep_config.stats_interval},
                    {"tx_buff_limit", cfg.bep_config.tx_buff_limit},
                    {"tx_timeout", cfg.bep_config.tx_timeout},
                }}},
        {"dialer", toml::table{{
                       {"enabled", cfg.dialer_config.enabled},
                       {"redial_timeout", cfg.dialer_config.redial_timeout},
                       {"skip_discovers", cfg.dialer_config.skip_discovers},
                   }}},
        {"fs", toml::table{{
                   {"temporally_timeout", cfg.fs_config.temporally_timeout},
                   {"mru_size", cfg.fs_config.mru_size},
                   {"bytes_scan_iteration_limit", cfg.fs_config.bytes_scan_iteration_limit},
                   {"files_scan_iteration_limit", cfg.fs_config.files_scan_iteration_limit},
               }}},
        {"db", toml::table{{
                   {"upper_limit", cfg.db_config.upper_limit},
                   {"uncommitted_threshold", cfg.db_config.uncommitted_threshold},
                   {"max_blocks_per_diff", cfg.db_config.max_blocks_per_diff},
                   {"max_files_per_diff", cfg.db_config.max_files_per_diff},
               }}},
        {"relay", toml::table{{
                      {"enabled", cfg.relay_config.enabled},
                      {"debug", cfg.relay_config.debug},
                      {"discovery_url", cfg.relay_config.discovery_url->buffer().data()},
                      {"rx_buff_size", cfg.relay_config.rx_buff_size},
                  }}},
        {"fltk", toml::table{{
                     {"level", get_level(cfg.fltk_config.level)},
                     {"display_deleted", cfg.fltk_config.display_deleted},
                     {"display_missing", cfg.fltk_config.display_missing},
                     {"display_colorized", cfg.fltk_config.display_colorized},
                     {"main_window_width", cfg.fltk_config.main_window_width},
                     {"main_window_height", cfg.fltk_config.main_window_height},
                     {"left_panel_share", cfg.fltk_config.left_panel_share},
                     {"bottom_panel_share", cfg.fltk_config.bottom_panel_share},
                     {"log_records_buffer", cfg.fltk_config.log_records_buffer},
                 }}},
    }};
    // clang-format on
    out << tbl;
    return outcome::success();
}

outcome::result<main_t> generate_config(const bfs::path &config_path) {
    auto dir = config_path.parent_path();
    sys::error_code ec;
    bool exists = bfs::exists(dir, ec);
    if (!exists) {
        spdlog::info("creating directory {}", dir.string());
        bfs::create_directories(dir, ec);
        if (ec) {
            spdlog::error("cannot create dirs: {}", ec.message());
            return ec;
        }
    }

    std::string cert_file = home_path + "/cert.pem";
    std::string key_file = home_path + "/key.pem";
    auto config_dir_opt = utils::get_default_config_dir();
    if (!config_dir_opt) {
        auto ec = config_dir_opt.assume_error();
        auto msg = ec.message();
        spdlog::warn("cannot get config dir: {}", msg);
        return ec;
    }
    auto &config_dir = config_dir_opt.assume_value();
    bool is_home = dir == config_dir;
    if (!is_home) {
        using boost::algorithm::replace_all_copy;
        cert_file = replace_all_copy(cert_file, home_path, dir.string());
        key_file = replace_all_copy(key_file, home_path, dir.string());
    }

    auto device = get_device_name();

    // clang-format off
    main_t cfg;
    cfg.config_path = config_path;
    cfg.default_location = config_dir / "shared_data";
    cfg.timeout = 5000;
    cfg.device_name = device;
    cfg.hasher_threads = 3;
    cfg.log_configs = {
        // log_config_t {
        //     "default", spdlog::level::level_enum::trace, {"stdout"}
        // }
    };
    cfg.local_announce_config = local_announce_config_t {
        true,   /* enabled */
        21027,  /* port */
        30000   /* frequency */
    };
    cfg.global_announce_config = global_announce_config_t{
        true,                                                   /* enabled */
        false,                                                  /* debug */
        utils::parse("https://discovery.syncthing.net/v2/"),    /* announce_url */
        "LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
        cert_file,
        key_file,
        32 * 1024,
        3000,
        10 * 60,
    };
    cfg.upnp_config = upnp_config_t {
        true,       /* enabled */
        false,      /* debug */
        1,          /* max_wait */
        22001,      /* external port */
        64 * 1024   /* rx_buff */
    };
    cfg.bep_config = bep_config_t {
        16 * 1024 * 1024,   /* rx_buff_size */
        8 * 1024 * 1024,    /* tx_buff_limit */
        5000,               /* connect_timeout */
        60000,              /* request_timeout */
        90000,              /* tx_timeout */
        300000,             /* rx_timeout */
        16,                 /* blocks_max_requested */
        32,                 /* blocks_simultaneous_write */
        10,                 /* advances_per_iteration */
        100,                /* stats_interval */
    };
    cfg.dialer_config = dialer_config_t {
        true,       /* enabled */
        5 * 60000,  /* redial timeout */
        10          /* skip_discovers */
    };
    cfg.fs_config = fs_config_t {
        86400000,   /* temporally_timeout, 24h default */
        128,        /* mru_size max number of open files for reading and writing */
        1024*1024,  /* bytes_scan_iteration_limit max number of bytes before emitting scan events */
        128,        /* files_scan_iteration_limit max number processed files before emitting scan events */
    };
    cfg.db_config = db_config_t {
        0x0,           /* upper_limit, auto-adjust */
        150,           /* uncommitted_threshold */
        8192,          /* max blocks per diff */
        1024,          /* max files per diff */
    };

    cfg.relay_config = relay_config_t {
        true,                                                   /* enabled */
        false,                                                  /* debug */
        utils::parse("https://relays.syncthing.net/endpoint"),  /* discovery url */
        1024 * 1024,                                            /* rx buff size */
    };

    cfg.fltk_config = fltk_config_t {
        spdlog::level::level_enum::info,    /* level */
        false,                              /* display_deleted */
        true,                               /* display_missing */
        true,                               /* display_colorized */
        700,                                /* main_window_width */
        480,                                /* main_window_height */
        0.5,                                /* left_panel_share */
        0.3,                                /* bottom_panel_share */
        99'999,                             /* log_records_buffer */
    };
    return cfg;
}

}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once
#include <boost/outcome.hpp>
#include <filesystem>
#include "main.h"
#include "syncspirit-export.h"

namespace syncspirit::config {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

// comparators

using config_result_t = outcome::outcome<main_t, std::string>;

SYNCSPIRIT_API config_result_t get_config(std::istream &config, const bfs::path &config_path);

SYNCSPIRIT_API outcome::result<main_t> generate_config(const bfs::path &config_path);

SYNCSPIRIT_API outcome::result<void> serialize(const main_t cfg, std::ostream &out) noexcept;

} // namespace syncspirit::config

repo_name: basiliscos/syncspirit

license: mit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "constants.h"
#include "syncspirit-config.h"

namespace syncspirit::constants {

const char *client_name = "syncspirit";
const char *issuer_name = "syncthing";
const char *protocol_name = "bep/1.0";
const char *relay_protocol_name = "bep-relay";
const char *client_version = SYNCSPIRIT_VERSION;
const char *console_sink_env = "SYNCSPIRIT_CONSOLE_SINK";

} // namespace syncspirit::constants


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <cstdint>
#include "syncspirit-export.h"

namespace syncspirit::constants {

static const constexpr std::uint32_t bep_magic = 0x2EA7D90B;
static const constexpr std::uint32_t rescan_interval = 3600;
static const constexpr std::int_fast32_t tx_blocks_max_factor = 3;

SYNCSPIRIT_API extern const char *client_name;
SYNCSPIRIT_API extern const char *client_version;
SYNCSPIRIT_API extern const char *issuer_name;
SYNCSPIRIT_API extern const char *protocol_name;
SYNCSPIRIT_API extern const char *relay_protocol_name;
SYNCSPIRIT_API extern const char *console_sink_env;

} // namespace syncspirit::constants


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "cursor.h"
#include <algorithm>

namespace syncspirit::db {

cursor_t::cursor_t(cursor_t &&other) noexcept { std::swap(impl, other.impl); }

cursor_t::~cursor_t() {
    if (impl) {
        mdbx_cursor_close(impl);
    }
}

} // namespace syncspirit::db


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <mdbx.h>
#include "syncspirit-export.h"
#include "utils/bytes.h"
#include <boost/outcome.hpp>

namespace syncspirit {
namespace db {

namespace outcome = boost::outcome_v2;

struct transaction_t;

struct SYNCSPIRIT_API cursor_t {

    cursor_t() noexcept : impl{nullptr} {};
    cursor_t(cursor_t &&other) noexcept;
    ~cursor_t();

    template <typename F> outcome::result<void> iterate(unsigned char prefix, F &&f) noexcept {
        MDBX_val value;
        MDBX_val key;
        key.iov_base = (void *)(&prefix);
        key.iov_len = 1;
        auto r = mdbx_cursor_get(impl, &key, &value, MDBX_SET);
        if (r != MDBX_SUCCESS) {
            return outcome::success();
        }
        while ((r = mdbx_cursor_get(impl, &key, &value, MDBX_NEXT)) == MDBX_SUCCESS) {
            auto k = utils::bytes_view_t(reinterpret_cast<const unsigned char *>(key.iov_base), key.iov_len);
            if (k.front() > prefix) {
                break;
            }
            auto v = utils::bytes_view_t(reinterpret_cast<const unsigned char *>(value.iov_base), value.iov_len);
            auto rr = f(k, v);
            if (!rr) {
                return rr.error();
            }
        }
        return outcome::success();
    }

  private:
    MDBX_cursor *impl = nullptr;
    cursor_t(MDBX_cursor *impl_) noexcept : impl{impl_} {}
    friend struct transaction_t;
};

} // namespace db
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "error_code.h"
#include "mdbx.h"

namespace syncspirit::db {

const static detail::db_code_category category;
const static detail::mdbx_code_category mdbx_category;

const detail::db_code_category &db_code_category() { return category; }
const detail::mdbx_code_category &mdbx_code_category() { return mdbx_category; }

namespace detail {

const char *db_code_category::name() const noexcept { return "syncspirit_db_error"; }

std::string db_code_category::message(int c) const { return mdbx_strerror(c); }

const char *mdbx_code_category::name() const noexcept { return "syncspirit_mdbx_error"; }

std::string mdbx_code_category::message(int c) const {
    std::string r;
    switch (static_cast<error_code>(c)) {
    case error_code::success:
        r = "success";
        break;
    case error_code::db_version_size_mismatch:
        r = "db version size mismatch";
        break;
    default:
        r = "unknown";
    }
    r += " (";
    r += std::to_string(c) + ")";
    return r;
}

} // namespace detail
} // namespace syncspirit::db


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include <boost/system/error_code.hpp>
#include "syncspirit-export.h"

namespace syncspirit {
namespace db {

enum class error_code {
    success = 0,
    db_version_size_mismatch,
    deserialization_falure,
    invalid_device_id,
    local_device_not_found,
};

namespace detail {

class SYNCSPIRIT_API db_code_category : public boost::system::error_category {
    virtual const char *name() const noexcept override;
    virtual std::string message(int c) const override;
};

class SYNCSPIRIT_API mdbx_code_category : public boost::system::error_category {
    virtual const char *name() const noexcept override;
    virtual std::string message(int c) const override;
};

} // namespace detail

SYNCSPIRIT_API const detail::db_code_category &db_code_category();

SYNCSPIRIT_API const detail::mdbx_code_category &mdbx_code_category();

inline boost::system::error_code make_error_code(int e) { return {static_cast<int>(e), db_code_category()}; }
inline boost::system::error_code make_error_code(error_code ec) { return {static_cast<int>(ec), mdbx_code_category()}; }

} // namespace db
} // namespace syncspirit

namespace boost {
namespace system {

template <> struct is_error_code_enum<syncspirit::db::detail::db_code_category> : std::true_type {
    static const bool value = true;
};

template <> struct is_error_code_enum<syncspirit::db::detail::mdbx_code_category> : std::true_type {
    static const bool value = true;
};

} // namespace system
} // namespace boost


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2023 Ivan Baidakou

#include "prefix.h"

using namespace syncspirit::db;

static value_t mk(discr_t prefix, std::string_view name) noexcept {
    std::string r;
    r.resize(name.size() + 1);
    *r.data() = (char)prefix;
    std::copy(name.begin(), name.end(), r.begin() + 1);
    return r;
}

value_t prefixer_t<prefix::misc>::make(std::string_view name) noexcept { return mk(prefix::misc, name); }


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <mdbx.h>
#include <cstddef>
#include <string>

#include "syncspirit-export.h"

namespace syncspirit::db {

using discr_t = unsigned char;

namespace prefix {
// clang-format off
static const constexpr discr_t misc           {0x01};
static const constexpr discr_t device         {0x10};
static const constexpr discr_t folder         {0x11};
static const constexpr discr_t folder_info    {0x12};
static const constexpr discr_t file_info      {0x13};
static const constexpr discr_t ignored_device {0x14};
static const constexpr discr_t ignored_folder {0x15};
static const constexpr discr_t pending_folder {0x16};
static const constexpr discr_t block_info     {0x17};
static const constexpr discr_t pending_device {0x18};
// clang-format on
} // namespace prefix

struct SYNCSPIRIT_API value_t {
    std::string bytes;
    MDBX_val value;

    template <typename T> value_t(T &&bytes_) noexcept : bytes{std::forward<T>(bytes_)} {
        value.iov_base = bytes.data();
        value.iov_len = bytes.length();
    }

    inline operator MDBX_val *() noexcept { return &value; }
    inline operator const MDBX_val *() const noexcept { return &value; }
};

template <discr_t> struct prefixer_t;

template <> struct prefixer_t<prefix::misc> {
    static SYNCSPIRIT_API value_t make(std::string_view name) noexcept;
};

} // namespace syncspirit::db


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "transaction.h"
#include "error_code.h"

namespace syncspirit::db {

transaction_t::transaction_t(transaction_t &&other) noexcept {
    std::swap(type, other.type);
    std::swap(txn, other.txn);
    std::swap(dbi, other.dbi);
}

transaction_t &transaction_t::operator=(transaction_t &&other) noexcept {
    std::swap(type, other.type);
    std::swap(txn, other.txn);
    std::swap(dbi, other.dbi);
    return *this;
}

transaction_t::~transaction_t() {
    if (txn) {
        auto env = mdbx_txn_env(txn);
        mdbx_dbi_close(env, dbi);
        mdbx_txn_commit(txn);
    }
}

outcome::result<transaction_t> transaction_t::make(transaction_type_t type, MDBX_env *env_) noexcept {
    MDBX_txn *txn = nullptr;
    auto r = mdbx_txn_begin(env_, NULL, type == transaction_type_t::RO ? MDBX_TXN_RDONLY : MDBX_TXN_READWRITE, &txn);
    if (r != MDBX_SUCCESS) {
        goto FAILURE;
    }

    MDBX_dbi dbi;
    r = mdbx_dbi_open(txn, nullptr, MDBX_DB_DEFAULTS, &dbi);
    if (r != MDBX_SUCCESS) {
        goto FAILURE;
    }
    return outcome::success(transaction_t(type, txn, dbi));
FAILURE:
    if (txn) {
        mdbx_txn_abort(txn);
    }
    return outcome::failure(make_error_code(r));
}

outcome::result<void> transaction_t::commit() noexcept {
    auto r = mdbx_txn_commit(txn);
    if (r == MDBX_SUCCESS) {
        txn = nullptr;
        return outcome::success();
    }
    return outcome::failure(make_error_code(r));
}

outcome::result<cursor_t> transaction_t::cursor() noexcept {
    MDBX_cursor *c;
    auto r = mdbx_cursor_open(txn, dbi, &c);
    if (r != MDBX_SUCCESS) {
        return outcome::failure(make_error_code(r));
    }
    return cursor_t(c);
}

outcome::result<std::uint64_t> transaction_t::next_sequence() noexcept {
    std::uint64_t s;
    auto r = mdbx_dbi_sequence(txn, dbi, &s, 1);
    if (r != MDBX_SUCCESS) {
        return outcome::failure(make_error_code(r));
    }
    return s;
}

outcome::result<transaction_t> make_transaction(transaction_type_t type, MDBX_env *env_) noexcept {
    return transaction_t::make(type, env_);
}

outcome::result<transaction_t> make_transaction(transaction_type_t type, transaction_t &prev) noexcept {
    assert(prev.txn);
    auto env = mdbx_txn_env(prev.txn);
    auto r = prev.commit();
    if (!r) {
        return r.assume_error();
    }
    return make_transaction(type, env);
}

} // namespace syncspirit::db


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <mdbx.h>
#include "cursor.h"
#include <functional>
#include <boost/outcome.hpp>

namespace syncspirit {
namespace db {

namespace outcome = boost::outcome_v2;

struct transaction_t;

using tx_fn_t = std::function<bool(transaction_t &)>;

enum class transaction_type_t { RO, RW };

struct SYNCSPIRIT_API transaction_t {
    transaction_t() noexcept : txn{nullptr} {};
    transaction_t(transaction_t &&other) noexcept;
    ~transaction_t();

    transaction_t &operator=(transaction_t &&other) noexcept;

    outcome::result<void> commit() noexcept;
    outcome::result<cursor_t> cursor() noexcept;
    outcome::result<std::uint64_t> next_sequence() noexcept;

    static outcome::result<transaction_t> make(transaction_type_t type, MDBX_env *env_) noexcept;

    MDBX_txn *txn = nullptr;
    MDBX_dbi dbi;
    transaction_type_t type;

  private:
    transaction_t(transaction_type_t type_, MDBX_txn *txn_, MDBX_dbi dbi_) noexcept
        : txn{txn_}, dbi{dbi_}, type{type_} {}
};

SYNCSPIRIT_API outcome::result<transaction_t> make_transaction(transaction_type_t type, MDBX_env *env_) noexcept;
SYNCSPIRIT_API outcome::result<transaction_t> make_transaction(transaction_type_t type, transaction_t &prev) noexcept;

} // namespace db
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "utils.h"
#include "transaction.h"
#include "error_code.h"
#include "prefix.h"
#include "model/misc/error_code.h"
#include "proto/proto-helpers-db.h"
#include <boost/endian/conversion.hpp>

namespace syncspirit::db {

namespace be = boost::endian;

std::byte zero{0};
std::uint32_t version{2};

namespace misc {
static const constexpr std::string_view db_version = "db_version";
}

outcome::result<uint32_t> get_version(transaction_t &txn) noexcept {
    auto key = prefixer_t<prefix::misc>::make(misc::db_version);
    MDBX_val value;
    auto r = mdbx_get(txn.txn, txn.dbi, key, &value);
    if (r != MDBX_SUCCESS) {
        if (r == MDBX_NOTFOUND) {
            return outcome::success(0);
        }
        return make_error_code(r);
    }

    if (value.iov_len != sizeof(std::uint32_t)) {
        return make_error_code(error_code::db_version_size_mismatch);
    }

    std::uint32_t version;
    memcpy(&version, value.iov_base, sizeof(std::uint32_t));
    be::big_to_native_inplace(version);
    return version;
}

outcome::result<void> save_version(std::uint32_t v, transaction_t &txn) noexcept {
    using prefixes_t = std::vector<discr_t>;
    auto key = prefixer_t<prefix::misc>::make(misc::db_version);
    MDBX_val value;
    auto db_ver = be::native_to_big(v);
    value.iov_base = &db_ver;
    value.iov_len = sizeof(db_ver);
    auto r = mdbx_put(txn.txn, txn.dbi, key, &value, MDBX_UPSERT);
    if (r != MDBX_SUCCESS) {
        return make_error_code(r);
    }
    return outcome::success();
}

static outcome::result<void> migrate0(model::device_ptr_t &device, transaction_t &txn) noexcept {
    auto version_r = save_version(1, txn);
    if (!version_r) {
        return version_r;
    }

    // make anchors
    using prefixes_t = std::vector<discr_t>;
    prefixes_t prefixes{prefix::device,         prefix::folder,         prefix::folder_info,
                        prefix::file_info,      prefix::ignored_device, prefix::ignored_folder,
                        prefix::pending_folder, prefix::block_info,     prefix::pending_device};
    for (auto &prefix : prefixes) {
        MDBX_val key, value;
        key.iov_base = &prefix;
        key.iov_len = sizeof(prefix);
        value.iov_base = &zero;
        value.iov_len = sizeof(zero);
        auto r = mdbx_put(txn.txn, txn.dbi, &key, &value, MDBX_UPSERT);
        if (r != MDBX_SUCCESS) {
            return make_error_code(r);
        }
    }

    auto device_key = device->get_key();
    auto device_data = device->serialize();

    MDBX_val device_db_key;
    device_db_key.iov_base = (void *)device_key.data();
    device_db_key.iov_len = device_key.size();

    MDBX_val device_db_value;
    device_db_value.iov_base = device_data.data();
    device_db_value.iov_len = device_data.size();

    auto r = mdbx_put(txn.txn, txn.dbi, &device_db_key, &device_db_value, MDBX_UPSERT);
    if (r != MDBX_SUCCESS) {
        return make_error_code(r);
    }
    return outcome::success();
}

static outcome::result<void> migrate1(model::device_ptr_t &device, transaction_t &txn) noexcept {
    auto fis_opt = db::load(db::prefix::folder_info, txn);
    if (!fis_opt) {
        return fis_opt.error();
    }

    auto &container = fis_opt.value();
    auto self_key = device->device_id().get_key();

    for (auto &pair : container) {
        auto &key = pair.key;

        auto db_fi = db::FolderInfo();
        if (auto left = db::decode(pair.value, db_fi); left) {
            using ec_t = model::error_code_t;
            return make_error_code(ec_t::folder_info_deserialization_failure);
        }
        db::set_introducer_device_key(db_fi, self_key);
        auto new_value = db::encode(db_fi);
        auto r = db::save({key, new_value}, txn);
        if (!r) {
            return r;
        }
    }

    return save_version(db::version, txn);
}

static outcome::result<void> do_migrate(uint32_t from, model::device_ptr_t &device, transaction_t &txn) noexcept {
    switch (from) {
    case 0:
        return migrate0(device, txn);
    case 1:
        return migrate1(device, txn);
    default:
        assert(0 && "impossibe migration to future version");
        std::terminate();
    }
}

outcome::result<void> migrate(uint32_t from, model::device_ptr_t device, transaction_t &txn) noexcept {
    while (from != version) {
        auto r = do_migrate(from, device, txn);
        if (!r)
            return r;
        if (!r)
            return r;
        ++from;
    }
    return txn.commit();
}

outcome::result<container_t> load(discr_t prefix, transaction_t &txn) noexcept {
    auto cursor_opt = txn.cursor();
    if (!cursor_opt) {
        return cursor_opt.error();
    }
    auto &cursor = cursor_opt.value();
    container_t container;
    auto r = cursor.iterate(prefix, [&](auto key, auto value) -> outcome::result<void> {
        container.push_back(pair_t{key, value});
        return outcome::success();
    });
    if (!r) {
        return r.error();
    }
    return outcome::success(std::move(container));
}

outcome::result<void> save(const pair_t &container, transaction_t &txn) noexcept {
    MDBX_val key;
    key.iov_base = (void *)container.key.data();
    key.iov_len = container.key.size();

    MDBX_val value;
    value.iov_base = (void *)container.value.data();
    value.iov_len = container.value.size();

    auto r = mdbx_put(txn.txn, txn.dbi, &key, &value, MDBX_UPSERT);
    if (r != MDBX_SUCCESS) {
        return make_error_code(r);
    }
    return outcome::success();
}

outcome::result<void> remove(utils::bytes_view_t key_, transaction_t &txn) noexcept {
    MDBX_val key;
    key.iov_base = (void *)key_.data();
    key.iov_len = key_.size();

    auto r = mdbx_del(txn.txn, txn.dbi, &key, nullptr);
    if (r != MDBX_SUCCESS) {
        return make_error_code(r);
    }
    return outcome::success();
}

} // namespace syncspirit::db


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "utils/platform.h"
#include "transaction.h"
#include "prefix.h"
#include "model/diff/load/common.h"
#include "model/device.h"
#include "utils/bytes.h"
#include <boost/outcome.hpp>

namespace syncspirit {
namespace db {

extern std::uint32_t version;

using pair_t = model::diff::load::pair_t;
using container_t = model::diff::load::container_t;

SYNCSPIRIT_API outcome::result<std::uint32_t> get_version(transaction_t &txn) noexcept;
SYNCSPIRIT_API outcome::result<void> save_version(std::uint32_t v, transaction_t &txn) noexcept;
SYNCSPIRIT_API outcome::result<void> migrate(std::uint32_t from, model::device_ptr_t device,
                                             transaction_t &txn) noexcept;

SYNCSPIRIT_API outcome::result<container_t> load(discr_t prefix, transaction_t &txn) noexcept;
SYNCSPIRIT_API outcome::result<void> save(const pair_t &container, transaction_t &txn) noexcept;
SYNCSPIRIT_API outcome::result<void> remove(utils::bytes_view_t key, transaction_t &txn) noexcept;

} // namespace db
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023-2025 Ivan Baidakou

#include "chunk_iterator.h"

using namespace syncspirit::fs;

chunk_iterator_t::chunk_iterator_t(scan_task_ptr_t task_, model::file_info_ptr_t file_, file_ptr_t backend_) noexcept
    : task{std::move(task_)}, peer_file{std::move(file_)}, backend{std::move(backend_)}, last_queued_block{0},
      valid_blocks_count{0}, abandoned{false} {
    unhashed_blocks = peer_file->get_blocks().size();
    valid_blocks_map.resize(unhashed_blocks);
}

bool chunk_iterator_t::has_more_chunks() const noexcept {
    return !abandoned && (last_queued_block < (int64_t)peer_file->get_blocks().size());
}

auto chunk_iterator_t::read() noexcept -> outcome::result<details::chunk_t> {
    assert(!abandoned);
    auto &i = last_queued_block;
    auto block_sz = peer_file->get_block_size();
    auto file_sz = peer_file->get_size();
    auto next_size = ((i + 1) * block_sz) > file_sz ? file_sz - (i * block_sz) : block_sz;
    auto block_opt = backend->read(i * block_sz, next_size);
    if (!block_opt) {
        auto ec = block_opt.assume_error();
        abandoned = true;

        return outcome::failure(ec);
    } else {
        auto &block = block_opt.value();
        auto idx = i++;
        return outcome::success(details::chunk_t{std::move(block), (size_t)idx});
    }
}

void chunk_iterator_t::ack_hashing() noexcept { --unhashed_blocks; }

void chunk_iterator_t::ack_block(utils::bytes_view_t digest, size_t block_index) noexcept {
    auto &orig_block = peer_file->get_blocks().at(block_index);
    if (orig_block->get_hash() != digest) {
        return;
    }
    valid_blocks_map[block_index] = true;
    ++valid_blocks_count;
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023-2025 Ivan Baidakou

#pragma once

#include <boost/outcome.hpp>
#include <rotor/address.hpp>
#include <vector>

#include "model/file_info.h"
#include "utils/bytes.h"
#include "file.h"
#include "scan_task.h"
#include "syncspirit-export.h"

namespace syncspirit::fs {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

struct SYNCSPIRIT_API chunk_iterator_t {
    using valid_blocks_map_t = std::vector<bool>;

    chunk_iterator_t(scan_task_ptr_t task, model::file_info_ptr_t file, file_ptr_t backend) noexcept;

    bool has_more_chunks() const noexcept;
    inline bool is_complete() const noexcept { return unhashed_blocks == 0; }
    inline bool has_valid_blocks() const noexcept { return valid_blocks_count > 0; }
    inline const valid_blocks_map_t &valid_blocks() const noexcept { return valid_blocks_map; }

    void ack_hashing() noexcept;
    void ack_block(utils::bytes_view_t digest, size_t block_index) noexcept;
    outcome::result<details::chunk_t> read() noexcept;

    inline model::file_info_ptr_t get_file() { return peer_file; }

    inline bfs::path get_path() noexcept { return backend->get_path(); }
    inline outcome::result<void> remove() noexcept { return backend->remove(); }

    inline scan_task_ptr_t get_task() noexcept { return task; }

  private:
    scan_task_ptr_t task;
    model::file_info_ptr_t peer_file;
    file_ptr_t backend;
    int64_t last_queued_block;
    size_t unhashed_blocks;
    valid_blocks_map_t valid_blocks_map;
    std::uint_fast32_t valid_blocks_count;
    bool abandoned;
};

} // namespace syncspirit::fs


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "file.h"
#include "utils.h"
#include "utils/log.h"
#include <errno.h>
#include <cassert>
#include <sys/stat.h>
#include <sys/types.h>

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#include <io.h>
#else
#include <unistd.h>
#endif

using namespace syncspirit::fs;

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#define SS_STAT_FN(PATH, BUFF) _stat((PATH), (BUFF))
#define SS_STAT_BUFF struct _stat
#define SS_TRUNCATE(FD, SIZE) _chsize_s((FD), (SIZE))
#else
#define SS_STAT_FN(PATH, BUFF) stat((PATH), (BUFF))
#define SS_STAT_BUFF struct stat
#define SS_TRUNCATE(FD, SIZE) ftruncate((FD), (SIZE))
#endif

auto file_t::open_write(model::file_info_ptr_t model) noexcept -> outcome::result<file_t> {
    using mode_t = utils::fstream_t;
    auto tmp = model->get_size() > 0;
    auto path = tmp ? make_temporal(model->get_path()) : model->get_path();
    path.make_preferred();
    auto path_str = path.string();

    bool need_resize = true;
    auto expected_size = (uint64_t)model->get_size();

    SS_STAT_BUFF stat_info;
    auto r = SS_STAT_FN(path_str.c_str(), &stat_info);
    if (r == 0) {
        need_resize = static_cast<uint64_t>(stat_info.st_size) == expected_size;
    }
    auto mode = mode_t::in | mode_t::out | mode_t::binary;
    if (need_resize) {
        if (r == -1) {
            auto file = utils::fstream_t(path, mode | mode_t::trunc);
            mode = mode & ~mode_t::trunc;
        }
        auto ec = std::error_code{};
        bfs::resize_file(path, expected_size, ec);
        if (ec) {
            return ec;
        }
    }

    auto file = utils::fstream_t(path, mode);
    if (!file) {
        return sys::error_code{errno, sys::system_category()};
    }

    return file_t(std::move(file), std::move(model), std::move(path), tmp);
}

auto file_t::open_read(const bfs::path &path) noexcept -> outcome::result<file_t> {
    auto file = utils::fstream_t(path, utils::fstream_t::binary | utils::fstream_t::in);
    if (!file) {
        return sys::error_code{errno, sys::system_category()};
    }
    return file_t(std::move(file), path);
}

file_t::file_t() noexcept {};

file_t::file_t(utils::fstream_t backend_, model::file_info_ptr_t model_, bfs::path path_, bool temporal_) noexcept
    : backend{new utils::fstream_t(std::move(backend_))}, model{std::move(model_)}, path{std::move(path_)},
      temporal{temporal_} {
    auto model_path = model->get_path();
    path_str = model_path.string();
}

file_t::file_t(utils::fstream_t backend_, bfs::path path_) noexcept
    : backend{new utils::fstream_t(std::move(backend_))}, path{std::move(path_)}, path_str{path.string()},
      temporal{false} {}

file_t::file_t(file_t &&other) noexcept : backend{nullptr} { *this = std::move(other); }

file_t &file_t::operator=(file_t &&other) noexcept {
    std::swap(backend, other.backend);
    std::swap(model, other.model);
    std::swap(path, other.path);
    std::swap(path_str, other.path_str);
    std::swap(temporal, other.temporal);
    return *this;
}

file_t::~file_t() {
    if (backend && model) {
        auto result = close(model->is_locally_available());
        if (!result) {
            auto log = utils::get_logger("fs.file");
            auto &ec = result.assume_error();
            log->warn("(ignored) error closing file '{}' : {}", path_str, ec.message());
        }
    }
}

std::string_view file_t::get_path_view() const noexcept { return path_str; }

const bfs::path &file_t::get_path() const noexcept { return path; }

auto file_t::close(bool remove_temporal, const bfs::path &local_name) noexcept -> outcome::result<void> {
    assert(backend && model && "close has sense for r/w mode");
    backend.reset();

    sys::error_code ec;
    auto orig_path = local_name.empty() ? model->get_path() : local_name;
    if (remove_temporal) {
        assert(temporal);
        bfs::rename(path, orig_path, ec);
        if (ec) {
            return ec;
        }
    } else if (orig_path != path) {
        orig_path = path;
    }

    auto modified = from_unix(model->get_modified_s());
    bfs::last_write_time(orig_path, modified, ec);
    if (ec) {
        return ec;
    }

    return outcome::success();
}

auto file_t::remove() noexcept -> outcome::result<void> {
    backend.reset();

    sys::error_code ec;
    bfs::remove(path, ec);
    return ec;
}

auto file_t::read(size_t offset, size_t size) const noexcept -> outcome::result<utils::bytes_t> {
    if (backend->tellg() != offset) {
        if (!backend->seekp((long)offset, std::ios_base::beg)) {
            return sys::errc::make_error_code(sys::errc::io_error);
        }
    }

    utils::bytes_t r;
    r.resize(size);
    if (!backend->read(reinterpret_cast<char *>(r.data()), size)) {
        return sys::errc::make_error_code(sys::errc::io_error);
    }
    if (backend->gcount() != size) {
        return sys::errc::make_error_code(sys::errc::io_error);
    }

    return r;
}

auto file_t::write(size_t offset, utils::bytes_view_t data) noexcept -> outcome::result<void> {
    assert(offset + data.size() <= (size_t)model->get_size());
    if (backend->tellp() != offset) {
        if (!backend->seekp((long)offset, std::ios_base::beg)) {
            return sys::errc::make_error_code(sys::errc::io_error);
        }
    }

    auto ptr = reinterpret_cast<const char *>(data.data());
    if (!backend->write(ptr, data.size())) {
        return sys::errc::make_error_code(sys::errc::io_error);
    }
    if (!backend->flush()) {
        return sys::errc::make_error_code(sys::errc::io_error);
    }
    return outcome::success();
}

auto file_t::copy(size_t my_offset, const file_t &from, size_t source_offset, size_t size) noexcept
    -> outcome::result<void> {
    auto in_opt = from.read(source_offset, size);
    if (!in_opt) {
        return in_opt.assume_error();
    }

    auto &in = in_opt.assume_value();
    return write(my_offset, in);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <filesystem>
#include <memory>
#include <boost/outcome.hpp>
#include "model/file_info.h"
#include "utils/io.h"
#include "utils/bytes.h"
#include "syncspirit-export.h"

namespace syncspirit::fs {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

namespace details {

struct chunk_t {
    utils::bytes_t data;
    size_t block_index;
};
} // namespace details

struct SYNCSPIRIT_API file_t : model::arc_base_t<file_t> {
    file_t() noexcept;
    file_t(file_t &) = delete;
    file_t(file_t &&) noexcept;
    ~file_t();

    file_t &operator=(file_t &&) noexcept;

    std::string_view get_path_view() const noexcept;
    const bfs::path &get_path() const noexcept;

    outcome::result<void> close(bool remove_temporal, const bfs::path &local_name = {}) noexcept;
    outcome::result<void> remove() noexcept;
    outcome::result<void> write(size_t offset, utils::bytes_view_t data) noexcept;
    outcome::result<void> copy(size_t my_offset, const file_t &from, size_t source_offset, size_t size) noexcept;
    outcome::result<utils::bytes_t> read(size_t offset, size_t size) const noexcept;

    static outcome::result<file_t> open_write(model::file_info_ptr_t model) noexcept;
    static outcome::result<file_t> open_read(const bfs::path &path) noexcept;

  private:
    using backend_ptr_t = std::unique_ptr<utils::fstream_t>;
    file_t(utils::fstream_t backend, model::file_info_ptr_t model, bfs::path path, bool temporal) noexcept;
    file_t(utils::fstream_t backend, bfs::path path) noexcept;

    backend_ptr_t backend;
    model::file_info_ptr_t model;
    bfs::path path;
    std::string path_str;
    bool temporal{false};
};

using file_ptr_t = model::intrusive_ptr_t<file_t>;

} // namespace syncspirit::fs


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "file_actor.h"
#include "net/names.h"
#include "model/folder_info.h"
#include "model/file_info.h"
#include "model/diff/modify/append_block.h"
#include "model/diff/modify/clone_block.h"
#include "model/diff/advance/remote_copy.h"
#include "model/diff/advance/remote_win.h"
#include "model/diff/modify/finish_file.h"
#include "utils.h"
#include "utils/io.h"
#include "utils/format.hpp"
#include "utils/platform.h"

using namespace syncspirit::fs;
using namespace syncspirit::proto;

file_actor_t::write_guard_t::write_guard_t(file_actor_t &actor_,
                                           const model::diff::modify::block_transaction_t &txn_) noexcept
    : actor{actor_}, txn{txn_}, success{false} {}

auto file_actor_t::write_guard_t::operator()(outcome::result<void> result) noexcept -> outcome::result<void> {
    success = (bool)result;
    if (!success) {
        actor.log->debug("I/O failure on {}: {}", txn.file_name, result.assume_error().message());
    }
    return result;
}

file_actor_t::write_guard_t::~write_guard_t() {
    auto reply = success ? txn.ack() : txn.rej();
    actor.send<model::payload::model_update_t>(actor.coordinator, std::move(reply));
}

file_actor_t::file_actor_t(config_t &cfg)
    : r::actor_base_t{cfg}, cluster{cfg.cluster}, sequencer(cfg.sequencer), rw_cache(cfg.mru_size),
      ro_cache(cfg.mru_size) {
    assert(sequencer);
}

void file_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity(net::names::fs_actor, false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.register_name(net::names::fs_actor, address);
        p.discover_name(net::names::coordinator, coordinator, true).link(false);
    });
    plugin.with_casted<r::plugin::starter_plugin_t>([&](auto &p) {
        p.subscribe_actor(&file_actor_t::on_block_request);
        p.subscribe_actor(&file_actor_t::on_model_update);
    });
}

void file_actor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    r::actor_base_t::on_start();
}

void file_actor_t::shutdown_start() noexcept {
    LOG_TRACE(log, "shutdown_start");
    r::actor_base_t::shutdown_start();
    rw_cache.clear();
}

void file_actor_t::on_model_update(model::message::model_update_t &message) noexcept {
    LOG_TRACE(log, "on_model_update");
    auto &payload = message.payload;
    auto &diff = payload.diff;
    auto r = diff->visit(*this, nullptr);
    if (!r) {
        auto ee = make_error(r.assume_error());
        return do_shutdown(ee);
    }
    send<model::payload::model_update_t>(coordinator, std::move(diff), payload.custom);
}

void file_actor_t::on_block_request(message::block_request_t &message) noexcept {
    LOG_TRACE(log, "on_block_request");
    auto &p = message.payload;
    auto &dest = p.reply_to;
    auto &req = message.payload.remote_request;
    auto folder = cluster->get_folders().by_id(get_folder(req));
    auto folder_info = folder->get_folder_infos().by_device(*cluster->get_device());
    auto file_info = folder_info->get_file_infos().by_name(get_name(req));
    auto &path = file_info->get_path();
    auto file_opt = open_file_ro(path, true);
    auto ec = sys::error_code{};
    auto data = utils::bytes_t{};
    if (!file_opt) {
        ec = file_opt.assume_error();
        LOG_ERROR(log, "error opening file {}: {}", path.string(), ec.message());
    } else {
        auto &file = file_opt.assume_value();
        auto offset = get_offset(req);
        auto size = get_size(req);
        auto block_opt = file->read(offset, size);
        if (!block_opt) {
            ec = block_opt.assume_error();
            LOG_WARN(log, "error requesting block; offset = {}, size = {} :: {} ", offset, size, ec.message());
        } else {
            data = std::move(block_opt.assume_value());
        }
    }
    send<payload::block_response_t>(dest, std::move(req), ec, std::move(data));
}

auto file_actor_t::reflect(model::file_info_ptr_t &file_ptr, const bfs::path &path) noexcept -> outcome::result<void> {
    auto &file = *file_ptr;
    sys::error_code ec;

    if (file.is_deleted()) {
        if (bfs::exists(path, ec)) {
            LOG_DEBUG(log, "removing {}", path.string());
            auto ok = bfs::remove_all(path, ec);
            if (!ok) {
                LOG_ERROR(log, "error removing {} : {}", path.string(), ec.message());
                return ec;
            }
        } else {
            LOG_TRACE(log, "{} already abscent, noop", path.string());
        }
        return outcome::success();
    }

    auto parent = path.parent_path();

    bool exists = bfs::exists(parent, ec);
    if (!exists) {
        bfs::create_directories(parent, ec);
        if (ec) {
            return ec;
        }
    }

    if (file.is_file()) {
        auto sz = file.get_size();
        if (file.is_locally_available() && sz) {
            return outcome::success();
        }

        bool temporal = sz > 0;
        if (temporal) {
            LOG_TRACE(log, "touching file {} ({} bytes)", path.string(), sz);
            auto file_opt = open_file_rw(path, &file);
            if (!file_opt) {
                auto &err = file_opt.assume_error();
                LOG_ERROR(log, "cannot open file: {}: {}", path.string(), err.message());
                return err;
            }
        } else {
            LOG_TRACE(log, "touching empty file {}", path.string());
            auto out = utils::ofstream_t(path, utils::ofstream_t::trunc);
            if (!out) {
                auto ec = sys::error_code{errno, sys::system_category()};
                LOG_ERROR(log, "error creating {}: {}", path.string(), ec.message());
                return ec;
            }
            out.close();
            bfs::last_write_time(path, from_unix(file.get_modified_s()), ec);
            if (ec) {
                return ec;
            }
        }
    } else if (file.is_dir()) {
        LOG_DEBUG(log, "creating directory {}", path.string());
        bfs::create_directory(path, ec);
        if (ec) {
            return ec;
        }
    } else if (file.is_link()) {
        if (utils::platform_t::symlinks_supported()) {
            auto target = bfs::path(file.get_link_target());
            LOG_DEBUG(log, "creating symlink {} -> {}", path.string(), target.string());

            bool attempt_create =
                !bfs::exists(path, ec) || !bfs::is_symlink(path, ec) || (bfs::read_symlink(path, ec) != target);
            if (attempt_create) {
                bfs::create_symlink(target, path, ec);
                if (ec) {
                    LOG_WARN(log, "error symlinking {} -> {} : {}", path.string(), target.string(), ec.message());
                    return ec;
                }
            } else {
                LOG_TRACE(log, "no need to create symlink {} -> {}", path.string(), target.string());
            }
        } else {
            LOG_TRACE(log, "symlinks are not supported by platform, no I/O for {}", path.string());
        }
    }

    return outcome::success();
}

auto file_actor_t::operator()(const model::diff::advance::remote_copy_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto file_info = folder->get_folder_infos().by_device_id(diff.peer_id);
    auto name = get_name(diff.proto_source);
    auto file = file_info->get_file_infos().by_name(name);
    auto r = reflect(file, file->get_path());
    return r ? diff.visit_next(*this, custom) : r;
}

auto file_actor_t::operator()(const model::diff::advance::remote_win_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto folder_info = folder->get_folder_infos();
    auto file_info = folder_info.by_device_id(diff.peer_id);
    auto source_name = get_name(diff.proto_source);
    auto local_name = get_name(diff.proto_local);
    auto file = file_info->get_file_infos().by_name(source_name);
    auto &source_path = file->get_path();
    auto target_path = folder->get_path() / local_name;
    LOG_DEBUG(log, "renaming {} -> {}", source_path, target_path);
    auto ec = sys::error_code{};
    bfs::rename(source_path, target_path);
    return !ec ? diff.visit_next(*this, custom) : ec;
}

auto file_actor_t::operator()(const model::diff::modify::finish_file_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto file_info = folder->get_folder_infos().by_device_id(diff.peer_id);
    auto file = file_info->get_file_infos().by_name(diff.file_name);
    auto local_path = file->get_path();
    auto path = local_path.string();
    auto action = diff.action;

    if (action == model::advance_action_t::resolve_remote_win) {
        auto &self = *cluster->get_device();
        auto local_fi = folder->get_folder_infos().by_device(self);
        auto local_file = local_fi->get_file_infos().by_name(diff.file_name);
        auto conflicting_name = local_file->make_conflicting_name();
        auto target_path = folder->get_path() / conflicting_name;
        auto ec = sys::error_code{};
        path = file->get_path().string();
        LOG_DEBUG(log, "renaming {} -> {}", file->get_name(), conflicting_name);
        bfs::rename(path, target_path);
        if (ec) {
            LOG_ERROR(log, "cannot rename file: {}: {}", path, ec.message());
            return ec;
        }
    }
    auto backend = rw_cache.get(path);
    if (!backend) {
        LOG_DEBUG(log, "attempt to flush non-opened file {}, re-open it as temporal", path);
        auto path_tmp = make_temporal(file->get_path());
        auto result = open_file_rw(path_tmp, file);
        if (!result) {
            auto &ec = result.assume_error();
            LOG_ERROR(log, "cannot open file: {}: {}", path_tmp.string(), ec.message());
            return ec;
        }
        backend = std::move(result.assume_value());
    }

    rw_cache.remove(backend);
    auto ok = backend->close(true, local_path);
    if (!ok) {
        auto &ec = ok.assume_error();
        LOG_ERROR(log, "cannot close file: {}: {}", path, ec.message());
        return ec;
    }

    LOG_INFO(log, "file {} ({} bytes) is now locally available", path, file->get_size());

    auto ack = model::diff::advance::advance_t::create(action, *file, *sequencer);
    send<model::payload::model_update_t>(coordinator, std::move(ack), this);
    return diff.visit_next(*this, custom);
}

auto file_actor_t::operator()(const model::diff::modify::append_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto guard = write_guard_t(*this, diff);
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto file_info = folder->get_folder_infos().by_device_id(diff.device_id);
    auto file = file_info->get_file_infos().by_name(diff.file_name);
    auto &path = file->get_path();
    auto path_str = path.string();
    auto file_opt = open_file_rw(path, file);
    if (!file_opt) {
        auto &err = file_opt.assume_error();
        LOG_ERROR(log, "cannot open file: {}: {}", path_str, err.message());
        return err;
    }

    auto block_index = diff.block_index;
    auto offset = file->get_block_offset(block_index);
    auto &backend = file_opt.value();
    auto r = guard(backend->write(offset, diff.data));
    return r ? diff.visit_next(*this, custom) : r;
}

auto file_actor_t::get_source_for_cloning(model::file_info_ptr_t &source, const file_ptr_t &target_backend) noexcept
    -> outcome::result<file_ptr_t> {
    auto source_path = source->get_path();
    if (source_path == target_backend->get_path()) {
        return target_backend;
    }

    auto source_tmp = make_temporal(source_path);

    if (auto cached = rw_cache.get(source_path.string()); cached) {
        return cached;
    } else if (auto cached = rw_cache.get(source_tmp.string()); cached) {
        return cached;
    } else if (auto cached = ro_cache.get(source_tmp.string()); cached) {
        return cached;
    } else if (auto cached = ro_cache.get(source_tmp.string()); cached) {
        return cached;
    } else if (auto opt = open_file_ro(source_tmp, false)) {
        return opt.assume_value();
    }

    return open_file_ro(source_path, false);
}

auto file_actor_t::operator()(const model::diff::modify::clone_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto guard = write_guard_t(*this, diff);
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto target_folder_info = folder->get_folder_infos().by_device_id(diff.device_id);
    auto target = target_folder_info->get_file_infos().by_name(diff.file_name);

    auto source_folder_info = folder->get_folder_infos().by_device_id(diff.source_device_id);
    auto source = source_folder_info->get_file_infos().by_name(diff.source_file_name);

    auto &target_path = target->get_path();
    auto file_opt = open_file_rw(target_path, target);
    if (!file_opt) {
        auto &err = file_opt.assume_error();
        LOG_ERROR(log, "cannot open file: {}: {}", target_path.string(), err.message());
        return err;
    }
    auto target_backend = std::move(file_opt.assume_value());
    auto source_backend_opt = get_source_for_cloning(source, target_backend);
    if (!source_backend_opt) {
        auto ec = source_backend_opt.assume_error();
        LOG_ERROR(log, "cannot open file for cloning: {}: {}", target_path.string(), ec.message());
    }

    auto &source_backend = source_backend_opt.assume_value();
    auto &block = source->get_blocks().at(diff.source_block_index);
    auto target_offset = target->get_block_offset(diff.block_index);
    auto source_offset = source->get_block_offset(diff.source_block_index);
    auto r = guard(target_backend->copy(target_offset, *source_backend, source_offset, block->get_size()));
    return r ? diff.visit_next(*this, custom) : r;
}

auto file_actor_t::open_file_rw(const std::filesystem::path &path, model::file_info_ptr_t info) noexcept
    -> outcome::result<file_ptr_t> {
    auto item = rw_cache.get(path.string());
    if (item) {
        return item;
    }

    auto size = info->get_size();
    LOG_TRACE(log, "open_file (model), path = {} ({} bytes)", path.string(), size);
    // auto opt = file_t::open_write(path, )
    // bfs::path operational_path = temporal ? make_temporal(path) : path;

    auto parent = path.parent_path();
    sys::error_code ec;

    bool exists = bfs::exists(parent, ec);
    if (!exists) {
        bfs::create_directories(parent, ec);
        if (ec) {
            return ec;
        }
    }

    auto option = file_t::open_write(info);
    if (!option) {
        return option.assume_error();
    }
    auto ptr = file_ptr_t(new file_t(std::move(option.assume_value())));
    rw_cache.put(ptr);
    return ptr;
}

auto file_actor_t::open_file_ro(const bfs::path &path, bool use_cache) noexcept -> outcome::result<file_ptr_t> {
    LOG_TRACE(log, "open_file (by path), path = {}", path.string());
    if (use_cache) {
        auto file = rw_cache.get(path.string());
        if (file) {
            return file;
        }
    }

    auto opt = file_t::open_read(path);
    if (!opt) {
        return opt.assume_error();
    }
    return file_ptr_t(new file_t(std::move(opt.assume_value())));
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once

#include "file.h"
#include "messages.h"
#include "model/cluster.h"
#include "model/messages.h"
#include "model/diff/cluster_visitor.h"
#include "model/misc/lru_cache.hpp"
#include "model/misc/sequencer.h"
#include "config/main.h"
#include "utils/log.h"
#include "utils.h"
#include <rotor.hpp>

namespace syncspirit {

namespace model::details {

template <> inline std::string_view get_lru_key<fs::file_ptr_t>(const fs::file_ptr_t &item) {
    return item->get_path_view();
}

} // namespace model::details

namespace fs {

namespace r = rotor;
namespace outcome = boost::outcome_v2;

struct SYNCSPIRIT_API file_actor_config_t : r::actor_config_t {
    model::cluster_ptr_t cluster;
    model::sequencer_ptr_t sequencer;
    size_t mru_size;
};

template <typename Actor> struct file_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&cluster(const model::cluster_ptr_t &value) && noexcept {
        parent_t::config.cluster = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&sequencer(model::sequencer_ptr_t value) && noexcept {
        parent_t::config.sequencer = std::move(value);
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }

    builder_t &&mru_size(size_t value) && noexcept {
        parent_t::config.mru_size = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API file_actor_t : public r::actor_base_t, private model::diff::cluster_visitor_t {
    using config_t = file_actor_config_t;
    template <typename Actor> using config_builder_t = file_actor_config_builder_t<Actor>;

    explicit file_actor_t(config_t &cfg);

    void on_start() noexcept override;
    void shutdown_start() noexcept override;
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;

  private:
    using cache_t = model::mru_list_t<file_ptr_t>;

    struct write_guard_t {
        write_guard_t(file_actor_t &actor, const model::diff::modify::block_transaction_t &txn) noexcept;
        ~write_guard_t();

        outcome::result<void> operator()(outcome::result<void> result) noexcept;

        file_actor_t &actor;
        const model::diff::modify::block_transaction_t &txn;
        bool success;
    };

    void on_model_update(model::message::model_update_t &message) noexcept;
    void on_block_request(message::block_request_t &message) noexcept;

    outcome::result<file_ptr_t> get_source_for_cloning(model::file_info_ptr_t &source,
                                                       const file_ptr_t &target_backend) noexcept;

    outcome::result<file_ptr_t> open_file_rw(const bfs::path &path, model::file_info_ptr_t info) noexcept;
    outcome::result<file_ptr_t> open_file_ro(const bfs::path &path, bool use_cache = false) noexcept;

    outcome::result<void> operator()(const model::diff::advance::remote_copy_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::advance::remote_win_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::finish_file_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::append_block_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::clone_block_t &, void *) noexcept override;

    outcome::result<void> reflect(model::file_info_ptr_t &file, const bfs::path &path) noexcept;

    model::cluster_ptr_t cluster;
    model::sequencer_ptr_t sequencer;
    utils::logger_t log;
    r::address_ptr_t coordinator;
    cache_t rw_cache;
    cache_t ro_cache;
};

} // namespace fs
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "fs_supervisor.h"
#include "net/names.h"
#include "scan_actor.h"
#include "scan_scheduler.h"
#include "file_actor.h"
#include "model/diff/load/load_cluster.h"
#include "model/diff/advance/advance.h"
#include "model/diff/modify/upsert_folder.h"
#include "model/diff/modify/upsert_folder_info.h"
#include "model/diff/peer/update_folder.h"
#include "presentation/folder_entity.h"

using namespace syncspirit::fs;
using namespace syncspirit::presentation;

namespace {
namespace resource {
r::plugin::resource_id_t model = 0;
}
} // namespace

fs_supervisor_t::fs_supervisor_t(config_t &cfg)
    : parent_t(cfg), sequencer(cfg.sequencer), fs_config{cfg.fs_config}, hasher_threads{cfg.hasher_threads} {}

void fs_supervisor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    parent_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity("fs.supervisor", false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.discover_name(net::names::coordinator, coordinator, true).link(false).callback([&](auto phase, auto &ee) {
            if (!ee && phase == r::plugin::registry_plugin_t::phase_t::linking) {
                auto p = get_plugin(r::plugin::starter_plugin_t::class_identity);
                auto plugin = static_cast<r::plugin::starter_plugin_t *>(p);
                plugin->subscribe_actor(&fs_supervisor_t::on_model_update, coordinator);
                request<model::payload::model_request_t>(coordinator).send(init_timeout);
                resources->acquire(resource::model);
            }
        });
    });

    plugin.with_casted<r::plugin::starter_plugin_t>(
        [&](auto &p) {
            p.subscribe_actor(&fs_supervisor_t::on_model_request);
            p.subscribe_actor(&fs_supervisor_t::on_model_response);
        },
        r::plugin::config_phase_t::PREINIT);
}

void fs_supervisor_t::launch() noexcept {
    LOG_DEBUG(log, "launching children actors");
    auto factory = [this](r::supervisor_t &, const r::address_ptr_t &spawner) -> r::actor_ptr_t {
        auto timeout = shutdown_timeout * 9 / 10;
        return create_actor<file_actor_t>()
            .cluster(cluster)
            .sequencer(sequencer)
            .mru_size(fs_config.mru_size)
            .timeout(timeout)
            .spawner_address(spawner)
            .finish();
    };
    spawn(factory).restart_period(r::pt::seconds{1}).restart_policy(r::restart_policy_t::fail_only).spawn();

    auto timeout = shutdown_timeout * 9 / 10;
    scan_actor = create_actor<scan_actor_t>()
                     .fs_config(fs_config)
                     .cluster(cluster)
                     .sequencer(sequencer)
                     .requested_hashes_limit(hasher_threads * 2)
                     .timeout(timeout)
                     .finish();

    create_actor<scan_scheduler_t>().cluster(cluster).timeout(timeout).finish();

    for (auto &l : launchers) {
        l(cluster);
    }
}

void fs_supervisor_t::on_model_request(model::message::model_request_t &req) noexcept {
    LOG_TRACE(log, "on_model_request");
    if (cluster) {
        LOG_TRACE(log, "already have cluster, share it");
        reply_to(req, cluster);
        return;
    }
    LOG_TRACE(log, "no cluster, delaying response");
    model_request = &req;
}

void fs_supervisor_t::on_model_response(model::message::model_response_t &res) noexcept {
    LOG_TRACE(log, "on_model_response");
    resources->release(resource::model);
    auto &ee = res.payload.ee;
    if (ee) {
        LOG_ERROR(log, "cannot get model: {}", ee->message());
        return do_shutdown(ee);
    }
    cluster = std::move(res.payload.res.cluster);
    if (model_request) {
        reply_to(*model_request, cluster);
    }
    launch();
}

void fs_supervisor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    r::actor_base_t::on_start();
}

void fs_supervisor_t::on_model_update(model::message::model_update_t &message) noexcept {
    LOG_TRACE(log, "on_model_update");
    auto &diff = *message.payload.diff;
    auto r = diff.apply(*cluster, *this);
    if (!r) {
        LOG_ERROR(log, "error applying model diff: {}", r.assume_error().message());
        auto ee = make_error(r.assume_error());
        return do_shutdown(ee);
    }

    r = diff.visit(*this, nullptr);
    if (!r) {
        LOG_ERROR(log, "{}, error visiting model: {}", identity, r.assume_error().message());
        do_shutdown(make_error(r.assume_error()));
    }
}

auto fs_supervisor_t::operator()(const model::diff::load::load_cluster_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    for (auto &it : cluster->get_folders()) {
        auto &folder = it.item;
        auto folder_entity = folder_entity_ptr_t(new folder_entity_t(folder));
        folder->set_augmentation(folder_entity);
    }
    return diff.visit_next(*this, custom);
}

auto fs_supervisor_t::operator()(const model::diff::modify::upsert_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder_id = db::get_id(diff.db);
    auto folder = cluster->get_folders().by_id(folder_id);
    if (!folder->get_augmentation()) {
        auto folder_entity = folder_entity_ptr_t(new folder_entity_t(folder));
        folder->set_augmentation(folder_entity);
    }
    return diff.visit_next(*this, custom);
}

auto fs_supervisor_t::operator()(const model::diff::modify::upsert_folder_info_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto r = diff.visit_next(*this, custom);
    auto &folder = *cluster->get_folders().by_id(diff.folder_id);
    auto &device = *cluster->get_devices().by_sha256(diff.device_id);
    auto folder_info = folder.is_shared_with(device);
    if (&device != cluster->get_device()) {
        auto augmentation = folder.get_augmentation().get();
        auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
        folder_entity->on_insert(*folder_info);
    }
    return r;
}

auto fs_supervisor_t::operator()(const model::diff::advance::advance_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto augmentation = folder->get_augmentation().get();
    auto folder_entity = static_cast<presentation::folder_entity_t *>(augmentation);
    if (folder_entity) {
        auto &folder_infos = folder->get_folder_infos();
        auto local_fi = folder_infos.by_device(*cluster->get_device());
        auto file_name = proto::get_name(diff.proto_local);
        auto local_file = local_fi->get_file_infos().by_name(file_name);
        if (local_file) {
            folder_entity->on_insert(*local_file);
        }
    }
    return diff.visit_next(*this, custom);
}

auto fs_supervisor_t::operator()(const model::diff::peer::update_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    auto folder = cluster->get_folders().by_id(diff.folder_id);
    auto folder_aug = folder->get_augmentation().get();
    auto folder_entity = static_cast<presentation::folder_entity_t *>(folder_aug);

    auto &devices_map = cluster->get_devices();
    auto peer = devices_map.by_sha256(diff.peer_id);
    auto &files_map = folder->get_folder_infos().by_device(*peer)->get_file_infos();

    for (auto &file : diff.files) {
        auto file_name = proto::get_name(file);
        auto file_info = files_map.by_name(file_name);
        auto augmentation = file_info->get_augmentation().get();
        if (!augmentation) {
            folder_entity->on_insert(*file_info);
        }
    }
    return diff.visit_next(*this, custom);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <functional>
#include "config/main.h"
#include "utils/log.h"
#include "model/messages.h"
#include "model/diff/apply_controller.h"
#include "model/diff/cluster_visitor.h"
#include "model/misc/sequencer.h"
#include "syncspirit-export.h"
#include <rotor/thread.hpp>

namespace syncspirit {
namespace fs {

namespace r = rotor;
namespace rth = rotor::thread;
namespace outcome = boost::outcome_v2;

struct SYNCSPIRIT_API fs_supervisor_config_t : r::supervisor_config_t {
    config::fs_config_t fs_config;
    uint32_t hasher_threads;
    model::sequencer_ptr_t sequencer;
};

template <typename Supervisor> struct fs_supervisor_config_builder_t : r::supervisor_config_builder_t<Supervisor> {
    using builder_t = typename Supervisor::template config_builder_t<Supervisor>;
    using parent_t = r::supervisor_config_builder_t<Supervisor>;
    using parent_t::parent_t;

    builder_t &&fs_config(const config::fs_config_t &value) && noexcept {
        parent_t::config.fs_config = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
    builder_t &&hasher_threads(uint32_t value) && noexcept {
        parent_t::config.hasher_threads = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
    builder_t &&sequencer(model::sequencer_ptr_t value) && noexcept {
        parent_t::config.sequencer = std::move(value);
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct SYNCSPIRIT_API fs_supervisor_t : rth::supervisor_thread_t,
                                        private model::diff::cluster_visitor_t,
                                        private model::diff::apply_controller_t {
    using launcher_t = std::function<void(model::cluster_ptr_t &)>;
    using parent_t = rth::supervisor_thread_t;
    using config_t = fs_supervisor_config_t;
    template <typename Supervisor> using config_builder_t = fs_supervisor_config_builder_t<Supervisor>;

    explicit fs_supervisor_t(config_t &cfg);
    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void on_start() noexcept override;
    template <typename F> void add_launcher(F &&launcher) noexcept { launchers.push_back(std::forward<F>(launcher)); }

  private:
    using model_request_ptr_t = r::intrusive_ptr_t<model::message::model_request_t>;
    using launchers_t = std::vector<launcher_t>;

    void on_model_request(model::message::model_request_t &req) noexcept;
    void on_model_response(model::message::model_response_t &res) noexcept;
    void on_model_update(model::message::model_update_t &message) noexcept;
    void launch() noexcept;

    outcome::result<void> operator()(const model::diff::load::load_cluster_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::upsert_folder_info_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::advance::advance_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::peer::update_folder_t &, void *) noexcept override;

    model::sequencer_ptr_t sequencer;
    model::cluster_ptr_t cluster;
    utils::logger_t log;
    config::fs_config_t fs_config;
    uint32_t hasher_threads;
    r::address_ptr_t coordinator;
    r::actor_ptr_t scan_actor;
    r::actor_ptr_t file_actor;
    model_request_ptr_t model_request;
    launchers_t launchers;
};

} // namespace fs
} // namespace syncspirit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <rotor.hpp>
#include "proto/bep_support.h"
#include "scan_task.h"
#include "file.h"
#include "chunk_iterator.h"
#include "new_chunk_iterator.h"
#include "utils/bytes.h"

namespace syncspirit::fs {

namespace r = rotor;

namespace payload {

struct scan_progress_t {
    scan_task_ptr_t task;
};

using rehash_needed_t = chunk_iterator_t;
using hash_anew_t = new_chunk_iterator_t;

struct block_request_t {
    proto::Request remote_request;
    r::address_ptr_t reply_to;
};

struct block_response_t {
    proto::Request remote_request;
    sys::error_code ec;
    utils::bytes_t data;
};

} // namespace payload

namespace message {

using scan_progress_t = r::message_t<payload::scan_progress_t>;
using rehash_needed_t = r::message_t<payload::rehash_needed_t>;
using hash_anew_t = r::message_t<payload::hash_anew_t>;
using block_request_t = r::message_t<payload::block_request_t>;
using block_response_t = r::message_t<payload::block_response_t>;

} // namespace message

} // namespace syncspirit::fs


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "new_chunk_iterator.h"
#include "utils.h"
#include <algorithm>

using namespace syncspirit::fs;

new_chunk_iterator_t::new_chunk_iterator_t(scan_task_ptr_t task_, proto::FileInfo metadata_,
                                           file_ptr_t backend_) noexcept
    : task{std::move(task_)}, metadata{std::move(metadata_)}, backend{std::move(backend_)}, next_idx{0}, offset{0} {
    if (proto::get_type(metadata) == proto::FileInfoType::FILE) {
        file_size = proto::get_size(metadata);
        auto bs = proto::get_block_size(metadata);
        auto div = syncspirit::fs::get_block_size(file_size, bs);
        unread_blocks = div.count;
        block_size = div.size;
        unread_bytes = file_size;
        hashes.resize(unread_blocks);
        assert(!(unread_bytes && !block_size));
    } else {
        file_size = 0;
        unread_blocks = 0;
        block_size = 0;
        unread_bytes = 0;
    }
}

bool new_chunk_iterator_t::is_complete() const noexcept {
    return !unread_blocks && !unread_bytes && unfinished.empty();
}

auto new_chunk_iterator_t::read() noexcept -> outcome::result<details::chunk_t> {
    assert(unread_bytes);
    size_t next_sz = std::min(block_size, unread_bytes);
    assert(next_sz);
    auto r = backend->read(offset, next_sz);
    if (r) {
        offset += next_sz;
        auto idx = next_idx++;
        auto data = std::move(r.assume_value());
        unfinished.insert(idx);
        if (next_sz) {
            unread_bytes -= next_sz;
            assert(unread_blocks);
            --unread_blocks;
        }
        return details::chunk_t{std::move(data), idx};
    }
    invalid = true;
    return r.assume_error();
}

bool new_chunk_iterator_t::has_more_chunks() const noexcept { return unread_bytes > 0; }

void new_chunk_iterator_t::ack(size_t block_index, uint32_t weak, utils::bytes_t hash, int32_t block_size) noexcept {
    assert(block_index < hashes.size());
    assert(unfinished.count(block_index));
    assert(!hash.empty());
    hashes[block_index] = block_hash_t{hash, weak, block_size};
    unfinished.erase(block_index);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023-2025 Ivan Baidakou

#pragma once

#include <boost/outcome.hpp>

#include "utils/bytes.h"
#include "file.h"
#include "scan_task.h"
#include "syncspirit-export.h"

namespace syncspirit::fs {

namespace outcome = boost::outcome_v2;
namespace bfs = std::filesystem;

struct SYNCSPIRIT_API new_chunk_iterator_t {
    struct block_hash_t {
        utils::bytes_t digest;
        uint32_t weak;
        int32_t size;
    };

    using hashes_t = std::vector<block_hash_t>;

    new_chunk_iterator_t(scan_task_ptr_t task, proto::FileInfo metadata, file_ptr_t backend) noexcept;

    bool has_more_chunks() const noexcept;
    outcome::result<details::chunk_t> read() noexcept;
    inline const bfs::path &get_path() noexcept { return backend->get_path(); }

    inline scan_task_ptr_t get_task() noexcept { return task; }
    void ack(size_t block_index, uint32_t weak, utils::bytes_t hash, int32_t block_size) noexcept;
    bool is_complete() const noexcept;
    inline hashes_t &get_hashes() noexcept { return hashes; }
    inline int64_t get_size() const noexcept { return file_size; }
    inline int64_t get_block_size() const noexcept { return block_size; }
    inline proto::FileInfo &get_metadata() noexcept { return metadata; }

  private:
    scan_task_ptr_t task;
    proto::FileInfo metadata;
    file_ptr_t backend;
    int64_t last_queued_block;
    int64_t valid_blocks;
    size_t unread_blocks;
    size_t block_size;
    size_t queue_size;
    size_t unread_bytes;
    size_t next_idx;
    int64_t file_size;
    size_t offset;
    std::set<std::int64_t> unfinished;
    hashes_t hashes;
    bool invalid;
};

} // namespace syncspirit::fs