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

#include "update_folder.h"
#include "model/misc/file_iterator.h"
#include "model/diff/modify/add_blocks.h"
#include "model/diff/modify/remove_blocks.h"
#include "model/diff/cluster_visitor.h"
#include "model/misc/error_code.h"
#include "proto/proto-helpers.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::model::diff::peer;

update_folder_t::update_folder_t(std::string_view folder_id_, utils::bytes_view_t peer_id_, files_t files_,
                                 uuids_t uuids, blocks_t blocks, orphaned_blocks_t::set_t removed_blocks) noexcept
    : folder_id{std::string(folder_id_)}, peer_id{peer_id_.begin(), peer_id_.end()}, files(std::move(files_)),
      uuids{std::move(uuids)} {
    LOG_DEBUG(log, "update_folder_t, folder = {}", folder_id);
    auto current = (cluster_diff_t *)(nullptr);
    if (!blocks.empty()) {
        current = assign_child(new modify::add_blocks_t(std::move(blocks)));
    }
    if (!removed_blocks.empty()) {
        auto ptr = new modify::remove_blocks_t(std::move(removed_blocks));
        if (current) {
            current->assign_sibling(ptr);
        } else {
            assign_sibling(ptr);
        }
    }
}

auto update_folder_t::apply_impl(cluster_t &cluster, apply_controller_t &controller) const noexcept
    -> outcome::result<void> {
    auto r = applicator_t::apply_child(cluster, controller);
    if (!r) {
        return r;
    }

    auto folder = cluster.get_folders().by_id(folder_id);
    auto folder_info = folder->get_folder_infos().by_device_id(peer_id);
    auto &bm = cluster.get_blocks();

    auto max_seq = folder_info->get_max_sequence();
    auto &fm = folder_info->get_file_infos();

    for (std::size_t i = 0; i < files.size(); ++i) {
        auto &f = files[i];

        auto &uuid = uuids[i];
        auto file = file_info_ptr_t{};
        auto opt = file_info_t::create(uuid, f, folder_info);
        if (!opt) {
            return opt.assume_error();
        }
        file = std::move(opt.assume_value());

        if (proto::get_size(f)) {
            auto blocks_count = proto::get_blocks_size(f);
            for (int i = 0; i < blocks_count; ++i) {
                auto &b = proto::get_blocks(f, i);
                auto hash = proto::get_hash(b);
                auto strict_hash = block_info_t::make_strict_hash(hash);
                auto block = bm.by_hash(strict_hash.get_hash());
                assert(block);
                file->assign_block(block, (size_t)i);
            }
        }
        if (auto prev_file = fm.by_name(file->get_name()); prev_file) {
            fm.remove(prev_file);
            prev_file->update(*file);
            file = std::move(prev_file);
            file->notify_update();
        }

        folder_info->add_strict(file);
    }

    LOG_TRACE(log, "update_folder_t, apply(); max seq: {} -> {}", max_seq, folder_info->get_max_sequence());

    r = applicator_t::apply_sibling(cluster, controller);

    if (auto iterator = folder_info->get_device()->get_iterator(); iterator) {
        iterator->on_upsert(folder_info);
    }

    folder_info->notify_update();
    return r;
}

auto update_folder_t::visit(cluster_visitor_t &visitor, void *custom) const noexcept -> outcome::result<void> {
    LOG_TRACE(log, "visiting update_folder_t", folder_id);
    return visitor(*this, custom);
}

using diff_t = diff::cluster_diff_ptr_t;

static auto construct(sequencer_t &sequencer, folder_info_ptr_t &folder_info, syncspirit::utils::bytes_view_t peer_id,
                      update_folder_t::files_t files, update_folder_t::blocks_t new_blocks) -> outcome::result<diff_t> {
    auto folder = folder_info->get_folder();

    auto uuids = update_folder_t::uuids_t{};
    uuids.reserve(files.size());
    auto &fm = folder_info->get_file_infos();
    auto orphaned_candidates = orphaned_blocks_t{};
    for (const auto &f : files) {
        auto file = file_info_ptr_t{};
        bu::uuid file_uuid;
        auto prev_file = fm.by_name(proto::get_name(f));
        if (prev_file) {
            assign(file_uuid, prev_file->get_uuid());
            orphaned_candidates.record(*prev_file);
        } else {
            file_uuid = sequencer.next_uuid();
        }
        uuids.emplace_back(file_uuid);
    }

    auto orphaned_blocks = orphaned_candidates.deduce();
    if (!orphaned_blocks.empty()) {
        for (auto &b : new_blocks) {
            auto hash = proto::get_hash(b);
            auto strict_hash = block_info_t::make_strict_hash(hash);
            auto it = orphaned_blocks.find(strict_hash.get_key());
            if (it != orphaned_blocks.end()) {
                orphaned_blocks.erase(it);
            }
        }
    }

    auto diff = diff_t(new update_folder_t(folder->get_id(), peer_id, std::move(files), std::move(uuids),
                                           std::move(new_blocks), std::move(orphaned_blocks)));
    return outcome::success(std::move(diff));
}

static auto instantiate(const cluster_t &cluster, sequencer_t &sequencer, const device_t &source,
                        const syncspirit::proto::IndexBase &message) noexcept -> outcome::result<diff_t> {
    auto folder_id = proto::get_folder(message);
    auto folder = cluster.get_folders().by_id(folder_id);
    if (!folder) {
        return make_error_code(error_code_t::folder_does_not_exist);
    }

    auto device_id = source.device_id().get_sha256();
    auto fi = folder->get_folder_infos().by_device_id(device_id);
    if (!fi) {
        return make_error_code(error_code_t::folder_is_not_shared);
    }

    auto log = update_folder_t::get_log();
    auto &blocks = cluster.get_blocks();
    update_folder_t::files_t files;
    update_folder_t::blocks_t new_blocks;
    auto prev_sequence = fi->get_max_sequence();
    auto files_count = proto::get_files_size(message);
    files.reserve(files_count);
    for (int i = 0; i < files_count; ++i) {
        auto &f = proto::get_files(message, i);
        auto name = proto::get_name(f);
        auto is_deleted = proto::get_deleted(f);
        auto is_invalid = proto::get_invalid(f);
        auto blocks_count = proto::get_blocks_size(f);
        auto is_downloadable = !is_deleted && !is_invalid;
        if (!is_downloadable && blocks_count) {
            LOG_WARN(log, "file {} should not have blocks", name);
            return make_error_code(error_code_t::unexpected_blocks);
        }
        auto sequence = proto::get_sequence(f);
        if (sequence <= prev_sequence) {
            LOG_WARN(log, "file '{}' has incorrect sequence", name, sequence);
            return make_error_code(error_code_t::invalid_sequence);
        }

        if (sequence <= 0) {
            LOG_WARN(log, "file '{}' has wrong sequence", name, sequence);
            return make_error_code(error_code_t::invalid_sequence);
        }
        auto file_size = proto::get_size(f);
        auto size_by_blocks = std::int64_t(0);
        for (int j = 0; j < blocks_count; ++j) {
            auto &b = proto::get_blocks(f, j);
            size_by_blocks += proto::get_size(b);
            auto hash = proto::get_hash(b);
            auto strict_hash = block_info_t::make_strict_hash(hash);
            if (!blocks.by_hash(strict_hash.get_hash())) {
                new_blocks.emplace_back(b);
            }
        }
        if (is_downloadable && file_size != size_by_blocks) {
            return make_error_code(error_code_t::mismatch_file_size);
        }
        auto &version = proto::get_version(f);
        if (!proto::get_counters_size(version)) {
            return make_error_code(error_code_t::missing_version);
        }

        files.emplace_back(f);
    }

    return construct(sequencer, fi, device_id, std::move(files), std::move(new_blocks));
}

auto update_folder_t::create(const cluster_t &cluster, sequencer_t &sequencer, const model::device_t &source,
                             const proto::IndexBase &message) noexcept -> outcome::result<cluster_diff_ptr_t> {
    return instantiate(cluster, sequencer, source, message);
}


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

#pragma once

#include "../cluster_diff.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"
#include "model/misc/orphaned_blocks.h"
#include "proto/proto-fwd.hpp"

namespace syncspirit::model::diff::peer {

struct SYNCSPIRIT_API update_folder_t final : cluster_diff_t {
    using files_t = std::vector<proto::FileInfo>;
    using uuids_t = std::vector<bu::uuid>;
    using blocks_t = std::vector<proto::BlockInfo>;

    static outcome::result<cluster_diff_ptr_t> create(const cluster_t &cluster, sequencer_t &sequencer,
                                                      const model::device_t &source,
                                                      const proto::IndexBase &message) noexcept;

    outcome::result<void> apply_impl(cluster_t &, apply_controller_t &) const noexcept override;
    outcome::result<void> visit(cluster_visitor_t &, void *) const noexcept override;

    update_folder_t(std::string_view folder_id, utils::bytes_view_t peer_id, files_t files, uuids_t uuids,
                    blocks_t new_blocks, orphaned_blocks_t::set_t removed_blocks) noexcept;

    std::string folder_id;
    utils::bytes_t peer_id;
    files_t files;
    uuids_t uuids;
};

} // namespace syncspirit::model::diff::peer

repo_name: basiliscos/syncspirit

license: mit


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

#include "cluster.h"
#include "file_info.h"
#include "folder_info.h"
#include "device.h"
#include "db/prefix.h"
#include "fs/utils.h"
#include "misc/error_code.h"
#include "misc/file_iterator.h"
#include "proto/proto-helpers.h"
#include "utils/bytes_comparator.hpp"
#include <zlib.h>
#include <spdlog/spdlog.h>
#include <boost/date_time/c_local_time_adjustor.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/date_time.hpp>
#include <algorithm>
#include <set>

namespace syncspirit::model {

static const auto empty_block = []() -> proto::BlockInfo {
    unsigned char digest[SHA256_DIGEST_LENGTH];
    unsigned char empty_data[1] = {0};
    auto weak_hash = adler32(0L, Z_NULL, 0);
    weak_hash = adler32(weak_hash, empty_data, 0);
    utils::digest(empty_data, 0, digest);
    auto digets_bytes = utils::bytes_view_t(digest, SHA256_DIGEST_LENGTH);
    auto block = proto::BlockInfo();
    proto::set_weak_hash(block, weak_hash);
    proto::set_hash(block, digets_bytes);
    return block;
}();

namespace pt = boost::posix_time;

static const constexpr char prefix = (char)(db::prefix::file_info);

auto file_info_t::decompose_key(utils::bytes_view_t key) -> decomposed_key_t {
    assert(key.size() == file_info_t::data_length);
    auto fi_key = key.subspan(1, uuid_length);
    auto file_id = key.subspan(1 + uuid_length);
    return {fi_key, file_id};
}

outcome::result<file_info_ptr_t> file_info_t::create(utils::bytes_view_t key, const db::FileInfo &data,
                                                     const folder_info_ptr_t &folder_info_) noexcept {
    if (key.size() != data_length) {
        return make_error_code(error_code_t::invalid_file_info_key_length);
    }
    if (key[0] != prefix) {
        return make_error_code(error_code_t::invalid_file_info_prefix);
    }

    auto ptr = file_info_ptr_t();
    ptr = new file_info_t(key, folder_info_);

    auto r = ptr->fields_update(data);
    if (!r) {
        return r.assume_error();
    }

    return outcome::success(std::move(ptr));
}

auto file_info_t::create(const bu::uuid &uuid_, const proto::FileInfo &info_,
                         const folder_info_ptr_t &folder_info_) noexcept -> outcome::result<file_info_ptr_t> {
    auto ptr = file_info_ptr_t();
    ptr = new file_info_t(uuid_, folder_info_);

    auto r = ptr->fields_update(info_);
    if (!r) {
        return r.assume_error();
    }

    return outcome::success(std::move(ptr));
}

static void fill(unsigned char *key, const bu::uuid &uuid, const folder_info_ptr_t &folder_info_) noexcept {
    key[0] = prefix;
    auto fi_key = folder_info_->get_uuid();
    std::copy(fi_key.begin(), fi_key.end(), key + 1);
    std::copy(uuid.begin(), uuid.end(), key + 1 + fi_key.size());
}

file_info_t::guard_t::guard_t(file_info_t &file_) noexcept : file{&file_} { file_.synchronizing_lock(); }

file_info_t::guard_t::~guard_t() { file->synchronizing_unlock(); }

file_info_t::file_info_t(utils::bytes_view_t key_, const folder_info_ptr_t &folder_info_) noexcept
    : folder_info{folder_info_.get()} {
    assert(key_.subspan(1, uuid_length) == folder_info->get_uuid());
    std::copy(key_.begin(), key_.end(), key);
}

file_info_t::file_info_t(const bu::uuid &uuid, const folder_info_ptr_t &folder_info_) noexcept
    : folder_info{folder_info_.get()} {
    fill(key, uuid, folder_info_);
}

file_info_t::~file_info_t() {
    for (auto &b : blocks) {
        if (!b) {
            continue;
        }
        auto indices = b->unlink(this);
        for (auto i : indices) {
            blocks[i].reset();
        }
    }
}

utils::bytes_t file_info_t::create_key(const bu::uuid &uuid, const folder_info_ptr_t &folder_info_) noexcept {
    utils::bytes_t key;
    key.resize(data_length);
    fill(key.data(), uuid, folder_info_);
    return key;
}

std::string_view file_info_t::get_name() const noexcept { return name; }

std::uint64_t file_info_t::get_block_offset(size_t block_index) const noexcept {
    assert(!blocks.empty());
    return block_size * block_index;
}

auto file_info_t::fields_update(const db::FileInfo &source) noexcept -> outcome::result<void> {
    name = db::get_name(source);
    sequence = db::get_sequence(source);
    type = db::get_type(source);
    set_size(db::get_size(source));
    permissions = db::get_permissions(source);
    modified_s = db::get_modified_s(source);
    modified_ns = db::get_modified_ns(source);
    modified_by = db::get_modified_by(source);
    if (db::get_deleted(source)) {
        flags |= flags_t::f_deleted;
    }
    if (db::get_invalid(source)) {
        flags |= flags_t::f_invalid;
    }
    if (db::get_no_permissions(source)) {
        flags |= flags_t::f_no_permissions;
    }

    symlink_target = db::get_symlink_target(source);

    version.reset(new version_t(db::get_version(source)));

    full_name = fmt::format("{}/{}", folder_info->get_folder()->get_label(), get_name());
    block_size = size ? db::get_block_size(source) : 0;
    auto block_count = db::get_blocks_size(source);
    return reserve_blocks(size ? block_count : 0);
}

auto file_info_t::fields_update(const proto::FileInfo &source) noexcept -> outcome::result<void> {
    name = proto::get_name(source);
    sequence = proto::get_sequence(source);
    type = proto::get_type(source);
    set_size(proto::get_size(source));
    permissions = proto::get_permissions(source);
    modified_s = proto::get_modified_s(source);
    modified_ns = proto::get_modified_ns(source);
    modified_by = proto::get_modified_by(source);
    if (proto::get_deleted(source)) {
        flags |= flags_t::f_deleted;
    }
    if (proto::get_invalid(source)) {
        flags |= flags_t::f_invalid;
    }
    if (proto::get_no_permissions(source)) {
        flags |= flags_t::f_no_permissions;
    }

    symlink_target = proto::get_symlink_target(source);

    version.reset(new version_t(proto::get_version(source)));

    full_name = fmt::format("{}/{}", folder_info->get_folder()->get_label(), get_name());
    block_size = size ? proto::get_block_size(source) : 0;
    auto block_count = proto::get_blocks_size(source);
    return reserve_blocks(size ? block_count : 0);
}

utils::bytes_view_t file_info_t::get_uuid() const noexcept { return {key + 1 + uuid_length, uuid_length}; }

void file_info_t::set_sequence(std::int64_t value) noexcept { sequence = value; }

db::FileInfo file_info_t::as_db(bool include_blocks) const noexcept {
    auto r = db::FileInfo();
    db::set_name(r, name);
    db::set_sequence(r, sequence);
    db::set_type(r, type);
    db::set_size(r, size);
    db::set_permissions(r, permissions);
    db::set_modified_s(r, modified_s);
    db::set_modified_ns(r, modified_ns);
    db::set_modified_by(r, modified_by);
    db::set_deleted(r, flags & f_deleted);
    db::set_invalid(r, flags & f_invalid);
    db::set_no_permissions(r, flags & f_no_permissions);
    db::set_version(r, version->as_proto());
    db::set_block_size(r, block_size);
    db::set_symlink_target(r, symlink_target);
    if (include_blocks) {
        for (auto &block : blocks) {
            if (!block) {
                continue;
            }
            db::add_blocks(r, block->get_hash());
        }
    }
    return r;
}

proto::FileInfo file_info_t::as_proto(bool include_blocks) const noexcept {
    auto r = proto::FileInfo();
    proto::set_name(r, name);
    proto::set_sequence(r, sequence);
    proto::set_type(r, type);
    proto::set_size(r, size);
    proto::set_permissions(r, permissions);
    proto::set_modified_s(r, modified_s);
    proto::set_modified_ns(r, modified_ns);
    proto::set_modified_by(r, modified_by);
    proto::set_deleted(r, flags & f_deleted);
    proto::set_invalid(r, flags & f_invalid);
    proto::set_no_permissions(r, flags & f_no_permissions);
    proto::set_version(r, version->as_proto());
    proto::set_block_size(r, block_size);
    proto::set_symlink_target(r, symlink_target);
    if (include_blocks) {
        size_t offset = 0;
        for (auto &b : blocks) {
            auto &block = *b;
            proto::add_blocks(r, block.as_bep(offset));
            offset += block.get_size();
        }
        if (blocks.empty() && is_file() && !is_deleted()) {
            proto::add_blocks(r, empty_block);
        }
    }
    return r;
}

outcome::result<void> file_info_t::reserve_blocks(size_t block_count) noexcept {
    size_t count = 0;
    if (!block_count && !(flags & f_deleted) && !(flags & f_invalid)) {
        if ((size < block_size) && (size >= (int64_t)fs::block_sizes[0])) {
            return make_error_code(error_code_t::invalid_block_size);
        }
        if (size) {
            if (!block_size) {
                return make_error_code(error_code_t::invalid_block_size);
            }
            count = size / block_size;
            if ((int64_t)(block_size * count) != size) {
                ++count;
            }
        }
    } else {
        count = block_count;
    }
    remove_blocks();
    blocks.resize(count);
    marks.resize(count);
    missing_blocks = !(flags & f_deleted) && !(flags & f_invalid) ? count : 0;
    return outcome::success();
}

utils::bytes_t file_info_t::serialize(bool include_blocks) const noexcept { return db::encode(as_db(include_blocks)); }

void file_info_t::mark_unreachable(bool value) noexcept {
    if (value) {
        flags |= f_unreachable;
    } else {
        flags &= ~f_unreachable;
    }
}

void file_info_t::mark_local() noexcept {
    flags = flags | f_local;
    auto self = folder_info->get_device();
    auto folder = folder_info->get_folder();
    for (auto it : folder->get_folder_infos()) {
        auto fi = it.item.get();
        auto peer = fi->get_device();
        if (peer != self) {
            auto fit = peer->get_iterator();
            if (fit) {
                auto peer_file = fi->get_file_infos().by_name(name);
                if (peer_file) {
                    fit->recheck(*peer_file);
                }
            }
        }
    }
}

void file_info_t::mark_local_available(size_t block_index) noexcept {
    assert(block_index < blocks.size());
    assert(!marks[block_index]);
    assert(missing_blocks);
    blocks[block_index]->mark_local_available(this);
    marks[block_index] = true;
    --missing_blocks;
}

bool file_info_t::is_locally_available(size_t block_index) const noexcept {
    assert(block_index < blocks.size());
    return marks[block_index];
}

bool file_info_t::is_locally_available() const noexcept { return missing_blocks == 0; }

bool file_info_t::is_partly_available() const noexcept { return missing_blocks < blocks.size(); }

const std::filesystem::path &file_info_t::get_path() const noexcept {
    if (!path) {
        path = folder_info->get_folder()->get_path() / boost::nowide::widen(name);
    }
    return path.value();
}

auto file_info_t::local_file() const noexcept -> file_info_ptr_t {
    auto device = folder_info->get_device();
    auto cluster = folder_info->get_folder()->get_cluster();
    auto &my_device = *cluster->get_device();
    assert(*device != my_device);
    (void)device;
    auto my_folder_info = folder_info->get_folder()->get_folder_infos().by_device(my_device);
    if (!my_folder_info) {
        return {};
    }

    auto local_file = my_folder_info->get_file_infos().by_name(get_name());
    return local_file;
}

void file_info_t::unlock() noexcept { flags = flags & ~flags_t::f_locked; }

void file_info_t::lock() noexcept { flags |= flags_t::f_locked; }

bool file_info_t::is_locked() const noexcept { return flags & flags_t::f_locked; }

void file_info_t::synchronizing_unlock() noexcept { flags = flags & ~flags_t::f_synchronizing; }

void file_info_t::synchronizing_lock() noexcept { flags |= flags_t::f_synchronizing; }

bool file_info_t::is_synchronizing() const noexcept { return flags & flags_t::f_synchronizing; }

bool file_info_t::is_unlocking() const noexcept { return flags & flags_t::f_unlocking; }

void file_info_t::set_unlocking(bool value) noexcept {
    if (value) {
        flags |= flags_t::f_unlocking;
    } else {
        flags = flags & ~flags_t::f_unlocking;
    }
}

void file_info_t::assign_block(const model::block_info_ptr_t &block, size_t index) noexcept {
    assert(index < blocks.size() && "blocks should be reserve enough space");
    assert(!blocks[index]);
    blocks[index] = block;
    block->link(this, index);
}

void file_info_t::remove_blocks() noexcept {
    for (auto &it : blocks) {
        remove_block(it);
    }
    std::fill_n(marks.begin(), blocks.size(), false);
    missing_blocks = blocks.size();
}

void file_info_t::remove_block(block_info_ptr_t &block) noexcept {
    if (!block) {
        return;
    }
    auto indices = block->unlink(this);
    for (auto i : indices) {
        blocks[i] = nullptr;
    }
}

std::int64_t file_info_t::get_size() const noexcept {
    if (type == proto::FileInfoType::FILE) {
        bool ok = !is_deleted() && !is_invalid();
        if (ok) {
            return size;
        }
    }
    return 0;
}

std::size_t file_info_t::expected_meta_size() const noexcept {
    auto r = name.size() + 1 + 8 + 4 + 8 + 4 + 8 + 3 + 8 + 4 + symlink_target.size();
    r += version->counters_size() * 16;
    r += blocks.size() * (8 + 4 + 4 + 32);
    return r;
}

std::uint32_t file_info_t::get_permissions() const noexcept { return permissions; }

bool file_info_t::has_no_permissions() const noexcept { return flags & f_no_permissions; }

void file_info_t::update(const file_info_t &other) noexcept {
    using hashes_t = std::set<utils::bytes_view_t, utils::bytes_comparator_t>;
    assert(this->get_key() == other.get_key());
    assert(this->name == other.name);
    type = other.type;
    size = other.size;
    permissions = other.permissions;
    modified_s = other.modified_s;
    modified_ns = other.modified_ns;
    modified_by = other.modified_by;
    flags = (other.flags & 0b111) | (flags & ~0b111); // local flags are preserved
    version = other.version;
    sequence = other.sequence;
    block_size = other.block_size;
    symlink_target = other.symlink_target;

    auto local_block_hashes = hashes_t{};
    for (auto &b : blocks) {
        if (b) {
            for (auto &fb : b->get_file_blocks()) {
                if (fb.is_locally_available() && fb.file() == this) {
                    local_block_hashes.emplace(b->get_hash());
                    break;
                }
            }
        }
    }
    // avoid use after free, as local block hashes have block_views
    auto blocks_copy = blocks;
    remove_blocks();

    marks = other.marks;
    blocks.resize(other.blocks.size());
    missing_blocks = blocks.size();
    for (size_t i = 0; i < other.blocks.size(); ++i) {
        auto &b = other.blocks[i];
        if (b) {
            assign_block(b, i);
            auto already_local = marks[i];
            if (already_local) {
                --missing_blocks;
            } else if (local_block_hashes.contains(b->get_hash())) {
                mark_local_available(i);
            }
        }
    }
}

bool file_info_t::is_global() const noexcept {
    auto self = folder_info->get_device();
    auto folder = folder_info->get_folder();
    for (auto &it : folder->get_folder_infos()) {
        if (it.item->get_device() == self) {
            continue;
        }
        auto &files = it.item->get_file_infos();
        auto file = files.by_name(name);
        if (!file) {
            continue;
        }
        auto other = file->get_version();
        if (!version->contains(*other)) {
            return false;
        }
    }
    return true;
}

std::string file_info_t::make_conflicting_name() const noexcept {
    using adjustor_t = boost::date_time::c_local_adjustor<pt::ptime>;
    auto path = bfs::path(name);
    auto file_name = path.filename();
    auto stem = file_name.stem().string();
    auto ext = file_name.extension().string();
    auto utc = pt::from_time_t(modified_s);
    auto local = adjustor_t::utc_to_local(utc);
    auto ymd = local.date().year_month_day();
    auto time = local.time_of_day();
    auto &counter = version->get_best();
    auto device_short = device_id_t::make_short(proto::get_id(counter));
    auto conflicted_name =
        fmt::format("{}.sync-conflict-{:04}{:02}{:02}-{:02}{:02}{:02}-{}{}", stem, (int)ymd.year, ymd.month.as_number(),
                    ymd.day.as_number(), time.hours(), time.minutes(), time.seconds(), device_short, ext);
    auto full_name = path.parent_path() / conflicted_name;
    return full_name.string();
}

auto file_info_t::guard() noexcept -> guard_ptr_t { return new guard_t(*this); }

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<0>(const file_info_ptr_t &item) noexcept {
    return item->get_uuid();
}
template <> SYNCSPIRIT_API utils::bytes_view_t get_index<1>(const file_info_ptr_t &item) noexcept {
    auto name = item->get_name();
    auto ptr = (unsigned char *)name.data();
    return {ptr, name.size()};
}

template <> SYNCSPIRIT_API std::int64_t get_index<2>(const file_info_ptr_t &item) noexcept {
    return item->get_sequence();
}

auto file_infos_map_t::sequence_projection() noexcept -> seq_projection_t { return key2item.template get<2>(); }

file_info_ptr_t file_infos_map_t::by_uuid(utils::bytes_view_t uuid) noexcept { return get<0>(uuid); }

file_info_ptr_t file_infos_map_t::by_name(std::string_view name) noexcept {
    auto ptr = (unsigned char *)name.data();
    auto view = utils::bytes_view_t(ptr, name.size());
    return get<1>(view);
}

file_info_ptr_t file_infos_map_t::by_sequence(std::int64_t value) noexcept { return get<2>(value); }

auto file_infos_map_t::range(std::int64_t lower, std::int64_t upper) noexcept -> range_t {
    auto &proj = sequence_projection();
    auto begin = proj.lower_bound(lower);
    auto end = proj.upper_bound(upper);
    return std::make_pair(begin, end);
}

} // namespace syncspirit::model


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

#pragma once

#include <cstdint>
#include <vector>
#include <optional>
#include <unordered_set>
#include <filesystem>
#include <boost/outcome.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include "misc/augmentation.h"
#include "misc/map.hpp"
#include "misc/uuid.h"
#include "block_info.h"
#include "version.h"
#include "proto/proto-fwd.hpp"
#include "syncspirit-export.h"

namespace syncspirit::model {

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

struct folder_info_t;
using folder_info_ptr_t = intrusive_ptr_t<folder_info_t>;

struct local_file_t;
struct blocks_iterator_t;

struct file_info_t;
using file_info_ptr_t = intrusive_ptr_t<file_info_t>;

struct SYNCSPIRIT_API file_info_t final : augmentable_t {

    // clang-format off
    enum flags_t {
        f_deleted        = 1 << 0,
        f_invalid        = 1 << 1,
        f_no_permissions = 1 << 2,
        f_locked         = 1 << 3,
        f_synchronizing  = 1 << 4,
        f_unreachable    = 1 << 5,
        f_unlocking      = 1 << 6,
        f_local          = 1 << 7,
    };
    // clang-format on

    using blocks_t = std::vector<block_info_ptr_t>;

    struct decomposed_key_t {
        utils::bytes_view_t folder_info_id;
        utils::bytes_view_t file_id;
    };

    struct guard_t : arc_base_t<guard_t> {
        guard_t(file_info_t &file) noexcept;
        ~guard_t();

        file_info_ptr_t file;
    };
    using guard_ptr_t = intrusive_ptr_t<guard_t>;

    static outcome::result<file_info_ptr_t> create(utils::bytes_view_t key, const db::FileInfo &data,
                                                   const folder_info_ptr_t &folder_info_) noexcept;
    static outcome::result<file_info_ptr_t> create(const bu::uuid &uuid, const proto::FileInfo &info_,
                                                   const folder_info_ptr_t &folder_info_) noexcept;
    static utils::bytes_t create_key(const bu::uuid &uuid, const folder_info_ptr_t &folder_info_) noexcept;

    static decomposed_key_t decompose_key(utils::bytes_view_t key);

    ~file_info_t();

    utils::bytes_view_t get_key() const noexcept { return utils::bytes_view_t(key, data_length); }
    utils::bytes_view_t get_uuid() const noexcept;
    bool operator==(const file_info_t &other) const noexcept { return get_uuid() == other.get_uuid(); }

    proto::FileInfo as_proto(bool include_blocks = true) const noexcept;
    db::FileInfo as_db(bool include_blocks = true) const noexcept;
    utils::bytes_t serialize(bool include_blocks = true) const noexcept;

    void update(const file_info_t &updated) noexcept;

    inline folder_info_t *get_folder_info() const noexcept { return folder_info; }
    std::string_view get_name() const noexcept;
    inline const std::string &get_full_name() const noexcept { return full_name; }
    inline version_ptr_t get_version() noexcept { return version; }
    inline const version_ptr_t &get_version() const noexcept { return version; }

    inline std::int64_t get_sequence() const noexcept { return sequence; }
    void set_sequence(std::int64_t value) noexcept;

    inline const blocks_t &get_blocks() const noexcept { return blocks; }

    void remove_blocks() noexcept;
    void assign_block(const model::block_info_ptr_t &block, size_t index) noexcept;

    inline proto::FileInfoType get_type() const noexcept { return type; }
    inline bool is_file() const noexcept { return type == proto::FileInfoType::FILE; }
    inline bool is_dir() const noexcept { return type == proto::FileInfoType::DIRECTORY; }
    inline bool is_link() const noexcept { return type == proto::FileInfoType::SYMLINK; }
    inline bool is_deleted() const noexcept { return flags & f_deleted; }
    inline bool is_invalid() const noexcept { return flags & f_invalid; }
    inline bool is_unreachable() const noexcept { return flags & f_unreachable; }
    inline bool is_local() const noexcept { return flags & f_local; }

    std::int64_t get_size() const noexcept;
    inline void set_size(std::int64_t value) noexcept { size = value; }

    std::int32_t get_block_size() const noexcept { return block_size; }
    std::uint64_t get_block_offset(size_t block_index) const noexcept;

    void mark_unreachable(bool value) noexcept;
    void mark_local_available(size_t block_index) noexcept;
    void mark_local() noexcept;
    bool is_locally_available(size_t block_index) const noexcept;
    bool is_locally_available() const noexcept;
    bool is_partly_available() const noexcept;

    const std::string &get_link_target() const noexcept { return symlink_target; }

    const bfs::path &get_path() const noexcept;

    inline std::int64_t get_modified_s() const noexcept { return modified_s; }
    inline std::int32_t get_modified_ns() const noexcept { return modified_ns; }
    inline std::uint64_t get_modified_by() const noexcept { return modified_by; }

    file_info_ptr_t local_file() const noexcept;

    bool is_locked() const noexcept;
    void lock() noexcept;
    void unlock() noexcept;

    bool is_global() const noexcept;

    bool is_synchronizing() const noexcept;
    void synchronizing_lock() noexcept;
    void synchronizing_unlock() noexcept;

    bool is_unlocking() const noexcept;
    void set_unlocking(bool value) noexcept;

    proto::FileInfo get() const noexcept;

    static const constexpr auto data_length = 1 + uuid_length * 2;

    outcome::result<void> fields_update(const db::FileInfo &) noexcept;
    outcome::result<void> fields_update(const proto::FileInfo &) noexcept;

    proto::Index generate() noexcept;
    std::size_t expected_meta_size() const noexcept;

    std::uint32_t get_permissions() const noexcept;
    bool has_no_permissions() const noexcept;

    guard_ptr_t guard() noexcept;

    std::string make_conflicting_name() const noexcept;

  private:
    using marks_vector_t = std::vector<bool>;

    file_info_t(utils::bytes_view_t key, const folder_info_ptr_t &folder_info_) noexcept;
    file_info_t(const bu::uuid &uuid, const folder_info_ptr_t &folder_info_) noexcept;
    outcome::result<void> reserve_blocks(size_t block_count) noexcept;

    void update_blocks(const proto::FileInfo &remote_info) noexcept;
    void remove_block(block_info_ptr_t &block) noexcept;

    unsigned char key[data_length];
    std::string name;
    folder_info_t *folder_info;
    proto::FileInfoType type;
    std::int64_t size;
    std::uint32_t permissions;
    std::int64_t modified_s;
    std::uint32_t modified_ns;
    std::uint64_t modified_by;

    int flags = 0;
    version_ptr_t version;
    std::int64_t sequence;
    std::int32_t block_size;
    std::string symlink_target;
    blocks_t blocks;
    mutable std::optional<bfs::path> path;
    std::string full_name;
    marks_vector_t marks;
    size_t missing_blocks;

    friend struct blocks_iterator_t;
};

namespace details {

template <> struct indexed_item_t<file_info_ptr_t, 3> {
    static constexpr size_t size = 3;
    using storage_t = std::tuple<utils::bytes_t, utils::bytes_t, std::int64_t>;
    using item_t = file_info_ptr_t;
    item_t item;
    mutable storage_t keys;

    indexed_item_t(const item_t &item_, const storage_t &keys_) noexcept : item{item_}, keys{keys_} {}

    template <size_t I> auto &get() noexcept { return std::get<I>(keys); }
};

using indexed_file_item_t = indexed_item_t<file_info_ptr_t, 3>;

template <> struct indexed_by<2, indexed_file_item_t> {
    using K = indexed_file_item_t;
    using type = mi::ordered_unique<mi::global_fun<const K &, single_key_t<K, 2>, &get_key<2, K>>>;
};

} // namespace details

struct SYNCSPIRIT_API file_infos_map_t : generic_map_t<file_info_ptr_t, 3> {
    using parent_t = generic_map_t<file_info_ptr_t, 3>;
    using seq_projection_t = std::remove_cv_t<decltype(std::declval<map_t>().template get<2>())>;
    using seq_iterator_t = decltype(std::declval<seq_projection_t>().begin());
    using range_t = std::pair<seq_iterator_t, seq_iterator_t>;

    file_info_ptr_t by_uuid(utils::bytes_view_t) noexcept;
    file_info_ptr_t by_name(std::string_view name) noexcept;
    file_info_ptr_t by_sequence(std::int64_t sequence) noexcept;
    seq_projection_t &sequence_projection() noexcept;
    range_t range(std::int64_t lower, std::int64_t upper) noexcept;
};

using file_infos_set_t = std::unordered_set<file_info_ptr_t>;

}; // namespace syncspirit::model

namespace std {

template <> struct hash<syncspirit::model::file_info_ptr_t> {
    inline size_t operator()(const syncspirit::model::file_info_ptr_t &file) const noexcept {
        return reinterpret_cast<size_t>(file.get());
    }
};

template <> struct hash<syncspirit::model::file_info_t::guard_ptr_t> {
    inline size_t operator()(const syncspirit::model::file_info_t::guard_ptr_t &guard) const noexcept {
        return reinterpret_cast<size_t>(guard->file.get());
    }
};
} // namespace std


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

#include "folder.h"
#include "db/utils.h"
#include "db/prefix.h"
#include "proto/proto-helpers.h"
#include "misc/error_code.h"
#include "utils/format.hpp"
#include <spdlog/spdlog.h>

namespace syncspirit::model {

static const constexpr char prefix = (char)(db::prefix::folder);

outcome::result<folder_ptr_t> folder_t::create(utils::bytes_view_t key, const db::Folder &folder) noexcept {
    if (key.size() != data_length) {
        return make_error_code(error_code_t::invalid_folder_key_length);
    }
    if (key[0] != prefix) {
        return make_error_code(error_code_t::invalid_folder_prefix);
    }

    auto ptr = folder_ptr_t();
    ptr = new folder_t(key);
    ptr->assign_fields(folder);
    return outcome::success(std::move(ptr));
}

outcome::result<folder_ptr_t> folder_t::create(const bu::uuid &uuid, const db::Folder &folder) noexcept {
    auto ptr = folder_ptr_t();
    ptr = new folder_t(uuid);
    ptr->assign_fields(folder);
    return outcome::success(std::move(ptr));
}

folder_t::folder_t(utils::bytes_view_t key_) noexcept : synchronizing{false}, suspended{false} {
    std::copy(key_.begin(), key_.end(), key);
}

folder_t::folder_t(const bu::uuid &uuid) noexcept : synchronizing{false}, suspended{false} {
    key[0] = prefix;
    std::copy(uuid.begin(), uuid.end(), key + 1);
}

utils::bytes_view_t folder_t::get_uuid() const noexcept { return utils::bytes_view_t(key + 1, uuid_length); }

void folder_t::add(const folder_info_ptr_t &folder_info) noexcept { folder_infos.put(folder_info); }

void folder_t::assign_cluster(const cluster_ptr_t &cluster_) noexcept { cluster = cluster_.get(); }

utils::bytes_t folder_t::serialize() noexcept {
    auto r = db::Folder();
    folder_data_t::serialize(r);
    return db::encode(r);
}

auto folder_t::is_shared_with(const model::device_t &device) const noexcept -> folder_info_ptr_t {
    return folder_infos.by_device_id(device.device_id().get_sha256());
}

std::optional<proto::Folder> folder_t::generate(const model::device_t &device) const noexcept {
    if (!is_shared_with(device)) {
        return {};
    }

    proto::Folder r;
    proto::set_id(r, id);
    proto::set_label(r, label);
    proto::set_read_only(r, folder_type == db::FolderType::send);
    proto::set_ignore_permissions(r, ignore_permissions);
    proto::set_ignore_delete(r, ignore_delete);
    proto::set_disable_temp_indexes(r, disable_temp_indixes);
    proto::set_paused(r, paused);
    for (auto &it : folder_infos) {
        auto &fi = *it.item;
        auto &d = *fi.get_device();
        auto introducer = d.is_introducer();
        auto &pd = proto::add_devices(r);
        proto::set_id(pd, d.device_id().get_sha256());
        proto::set_name(pd, d.get_name());
        proto::set_compression(pd, d.get_compression());
        if (auto cn = d.get_cert_name(); cn) {
            proto::set_cert_name(pd, cn.value());
        }
        std::int64_t max_seq = fi.get_max_sequence();
        proto::set_max_sequence(pd, max_seq);
        proto::set_index_id(pd, fi.get_index());
        proto::set_introducer(pd, introducer);
        proto::set_skip_introduction_removals(pd, d.get_skip_introduction_removals());
        spdlog::trace(
            "folder_t::generate (==>), folder = {} (index = 0x{:x}), device = {} (introducer: {}), max_seq = {}", label,
            fi.get_index(), d.device_id(), introducer, max_seq);
    }
    return r;
}

const pt::ptime &folder_t::get_scan_start() const noexcept { return scan_start; }
void folder_t::set_scan_start(const pt::ptime &value) noexcept { scan_start = value; }
const pt::ptime &folder_t::get_scan_finish() noexcept { return scan_finish; }
void folder_t::set_scan_finish(const pt::ptime &value) noexcept {
    assert(!scan_start.is_not_a_date_time());
    assert(scan_start <= value);
    scan_finish = value;
}

bool folder_t::is_scanning() const noexcept {
    if (scan_start.is_not_a_date_time()) {
        return false;
    }
    if (scan_finish.is_not_a_date_time()) {
        return true;
    }
    return scan_start > scan_finish;
}

bool folder_t::is_synchronizing() const noexcept { return synchronizing > 0; }

void folder_t::adjust_synchronization(std::int_fast32_t delta) noexcept {
    synchronizing += delta;
    assert(synchronizing >= 0);
}

void folder_t::mark_suspended(bool value) noexcept { suspended = value; }
bool folder_t::is_suspended() const noexcept { return suspended; }

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<0>(const folder_ptr_t &item) noexcept {
    return item->get_key();
}
template <> SYNCSPIRIT_API utils::bytes_view_t get_index<1>(const folder_ptr_t &item) noexcept {
    auto id = item->get_id();
    auto ptr = (unsigned char *)id.data();
    return {ptr, id.size()};
}

folder_ptr_t folders_map_t::by_id(std::string_view id) const noexcept {
    auto ptr = (unsigned char *)id.data();
    auto view = utils::bytes_view_t(ptr, id.size());
    return get<1>(view);
}

folder_ptr_t folders_map_t::by_key(utils::bytes_view_t key) const noexcept { return get<0>(key); }

} // namespace syncspirit::model


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

#pragma once

#include <cstdint>
#include <optional>
#include <boost/outcome.hpp>
#include "misc/augmentation.h"
#include "device.h"
#include "folder_info.h"
#include "misc/uuid.h"
#include "folder_data.h"
#include "syncspirit-export.h"
#include "proto/proto-fwd.hpp"

namespace syncspirit::model {

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

struct cluster_t;
using cluster_ptr_t = intrusive_ptr_t<cluster_t>;

struct folder_t;

using folder_ptr_t = intrusive_ptr_t<folder_t>;

struct SYNCSPIRIT_API folder_t final : augmentable_t, folder_data_t {
    static outcome::result<folder_ptr_t> create(utils::bytes_view_t key, const db::Folder &folder) noexcept;
    static outcome::result<folder_ptr_t> create(const bu::uuid &uuid, const db::Folder &folder) noexcept;

    using folder_data_t::assign_fields;
    using folder_data_t::serialize;

    void assign_cluster(const cluster_ptr_t &cluster) noexcept;
    void add(const folder_info_ptr_t &folder_info) noexcept;
    utils::bytes_t serialize() noexcept;

    bool operator==(const folder_t &other) const noexcept { return get_id() == other.get_id(); }
    bool operator!=(const folder_t &other) const noexcept { return !(*this == other); }

    folder_info_ptr_t is_shared_with(const device_t &device) const noexcept;

    utils::bytes_view_t get_key() const noexcept { return utils::bytes_view_t(key, data_length); }
    utils::bytes_view_t get_uuid() const noexcept;
    inline auto &get_folder_infos() noexcept { return folder_infos; }
    inline auto &get_folder_infos() const noexcept { return folder_infos; }
    inline cluster_t *&get_cluster() noexcept { return cluster; }
    const pt::ptime &get_scan_start() const noexcept;
    void set_scan_start(const pt::ptime &value) noexcept;
    const pt::ptime &get_scan_finish() noexcept;
    void set_scan_finish(const pt::ptime &value) noexcept;
    bool is_scanning() const noexcept;
    bool is_synchronizing() const noexcept;
    void adjust_synchronization(std::int_fast32_t delta) noexcept;
    void mark_suspended(bool value) noexcept;
    bool is_suspended() const noexcept;

    using folder_data_t::get_path;
    using folder_data_t::set_path;
    std::optional<proto::Folder> generate(const model::device_t &device) const noexcept;

    template <typename T> auto &access() noexcept;
    template <typename T> auto &access() const noexcept;

    static const constexpr size_t data_length = uuid_length + 1;

  private:
    folder_t(utils::bytes_view_t key) noexcept;
    folder_t(const bu::uuid &uuid) noexcept;

    pt::ptime scan_start;
    pt::ptime scan_finish;
    device_ptr_t device;
    folder_infos_map_t folder_infos;
    cluster_t *cluster = nullptr;
    unsigned char key[data_length];
    std::int_fast32_t synchronizing = 0;
    bool suspended;
};

struct SYNCSPIRIT_API folders_map_t : generic_map_t<folder_ptr_t, 2> {
    folder_ptr_t by_key(utils::bytes_view_t id) const noexcept;
    folder_ptr_t by_id(std::string_view id) const noexcept;
};

} // namespace syncspirit::model


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

#include "folder_data.h"
#include "proto/proto-helpers.h"

#include <boost/nowide/convert.hpp>

using namespace syncspirit::model;

void folder_data_t::assign_fields(const db::Folder &item) noexcept {
    id = db::get_id(item);
    label = db::get_label(item);
    path = boost::nowide::widen(db::get_path(item));
    folder_type = db::get_folder_type(item);
    rescan_interval = db::get_rescan_interval(item);
    pull_order = db::get_pull_order(item);
    scheduled = db::get_scheduled(item);
    ignore_permissions = db::get_ignore_permissions(item);
    ignore_delete = db::get_ignore_delete(item);
    disable_temp_indixes = db::get_disable_temp_indexes(item);
    paused = db::get_paused(item);
}

void folder_data_t::serialize(db::Folder &r) const noexcept {
    db::set_id(r, id);
    db::set_label(r, label);
    db::set_ignore_permissions(r, ignore_permissions);
    db::set_ignore_delete(r, ignore_delete);
    db::set_disable_temp_indexes(r, disable_temp_indixes);
    db::set_paused(r, paused);
    db::set_scheduled(r, scheduled);
    db::set_path(r, path.string());
    db::set_folder_type(r, folder_type);
    db::set_pull_order(r, pull_order);
    db::set_rescan_interval(r, rescan_interval);
}


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

#pragma once

#include <string>
#include <filesystem>
#include "syncspirit-export.h"
#include "proto/proto-fwd.hpp"

namespace syncspirit::model {

namespace bfs = std::filesystem;

struct SYNCSPIRIT_API folder_data_t {
    using folder_type_t = syncspirit::db::FolderType;
    using pull_order_t = syncspirit::db::PullOrder;

    inline const std::string &get_label() const noexcept { return label; }
    inline std::string_view get_id() const noexcept { return id; }
    inline void set_id(std::string_view value) noexcept { id = value; }
    inline bool is_read_only() const noexcept { return folder_type == folder_type_t::send; }
    inline bool is_deletion_ignored() const noexcept { return ignore_delete; }
    inline bool are_permissions_ignored() const noexcept { return ignore_permissions; }
    inline bool are_temp_indixes_disabled() const noexcept { return disable_temp_indixes; }
    inline bool is_paused() const noexcept { return paused; }
    inline bool is_scheduled() const noexcept { return scheduled; }
    inline folder_type_t get_folder_type() const noexcept { return folder_type; }
    inline pull_order_t get_pull_order() const noexcept { return pull_order; }
    inline const bfs::path &get_path() const noexcept { return path; }
    inline void set_path(const bfs::path &value) noexcept { path = value; }
    inline std::uint32_t get_rescan_interval() const noexcept { return rescan_interval; };
    inline void set_rescan_interval(std::uint32_t value) noexcept { rescan_interval = value; };

    void serialize(syncspirit::db::Folder &dest) const noexcept;

    template <typename T> auto &access() noexcept;
    template <typename T> auto &access() const noexcept;

  protected:
    void assign_fields(const db::Folder &item) noexcept;

    std::string id;
    std::string label;
    bfs::path path;
    folder_type_t folder_type;
    std::uint32_t rescan_interval;
    pull_order_t pull_order;
    bool scheduled;
    bool ignore_permissions;
    bool ignore_delete;
    bool disable_temp_indixes;
    bool paused;
};

} // namespace syncspirit::model


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

#include "folder_info.h"
#include "folder.h"
#include "cluster.h"
#include "proto/proto-helpers.h"
#include "../db/prefix.h"
#include "misc/error_code.h"
#include <spdlog/spdlog.h>
#include <algorithm>

namespace syncspirit::model {

static const constexpr char prefix = (char)(db::prefix::folder_info);

folder_info_t::decomposed_key_t::decomposed_key_t(utils::bytes_view_t reduced_key, utils::bytes_view_t folder_uuid,
                                                  utils::bytes_view_t folder_info_id_)
    : folder_info_id{folder_info_id_} {
    assert(reduced_key.size() == device_id_t::digest_length);
    assert(folder_uuid.size() == uuid_length);
    device_key_raw[0] = (char)(db::prefix::device);
    folder_key_raw[0] = (char)(db::prefix::folder);
    std::copy(begin(reduced_key), end(reduced_key), device_key_raw + 1);
    std::copy(begin(folder_uuid), end(folder_uuid), folder_key_raw + 1);
}

auto folder_info_t::decompose_key(utils::bytes_view_t key) -> decomposed_key_t {
    assert(key.size() == folder_info_t::data_length);
    auto device_key = key.subspan(1, device_id_t::digest_length);
    auto folder_id = key.subspan(1 + device_key.size(), uuid_length);
    auto folder_info_id = key.subspan(1 + device_key.size() + folder_id.size(), uuid_length);
    return decomposed_key_t(device_key, folder_id, folder_info_id);
}

outcome::result<folder_info_ptr_t> folder_info_t::create(utils::bytes_view_t key, const db::FolderInfo &data,
                                                         const device_ptr_t &device_,
                                                         const folder_ptr_t &folder_) noexcept {
    if (key.size() != data_length) {
        return make_error_code(error_code_t::invalid_folder_info_key_length);
    }
    if (key[0] != prefix) {
        return make_error_code(error_code_t::invalid_folder_info_prefix);
    }

    auto ptr = folder_info_ptr_t();
    ptr = new folder_info_t(key, device_, folder_);

    ptr->assign_fields(data);

    return outcome::success(std::move(ptr));
}

outcome::result<folder_info_ptr_t> folder_info_t::create(const bu::uuid &uuid, const db::FolderInfo &data,
                                                         const device_ptr_t &device_,
                                                         const folder_ptr_t &folder_) noexcept {
    auto ptr = folder_info_ptr_t();
    ptr = new folder_info_t(uuid, device_, folder_);

    ptr->assign_fields(data);

    return outcome::success(std::move(ptr));
}

folder_info_t::folder_info_t(utils::bytes_view_t key_, const device_ptr_t &device_,
                             const folder_ptr_t &folder_) noexcept
    : device{device_.get()}, folder{folder_.get()} {
    assert(key_.subspan(1, device_id_t::digest_length) == device->get_key().subspan(1));
    assert(key_.subspan(device_id_t::digest_length + 1, uuid_length) == folder->get_key().subspan(1));
    std::copy(key_.begin(), key_.end(), key);
}

folder_info_t::folder_info_t(const bu::uuid &uuid, const device_ptr_t &device_, const folder_ptr_t &folder_) noexcept
    : device{device_.get()}, folder{folder_.get()} {
    auto device_key = device->get_key().subspan(1);
    auto folder_key = folder->get_key().subspan(1);
    key[0] = prefix;
    std::copy(device_key.begin(), device_key.end(), key + 1);
    std::copy(folder_key.begin(), folder_key.end(), key + 1 + device_key.size());
    std::copy(uuid.begin(), uuid.end(), key + 1 + device_key.size() + folder_key.size());
}

void folder_info_t::assign_fields(const db::FolderInfo &fi) noexcept {
    index = db::get_index_id(fi);
    max_sequence = db::get_max_sequence(fi);
    introducer_device_key = db::get_introducer_device_key(fi);
}

utils::bytes_view_t folder_info_t::get_key() const noexcept { return utils::bytes_view_t(key, data_length); }

utils::bytes_view_t folder_info_t::get_uuid() const noexcept {
    return utils::bytes_view_t(key + 1 + device_id_t::digest_length + uuid_length, uuid_length);
}

bool folder_info_t::operator==(const folder_info_t &other) const noexcept {
    auto r = std::mismatch(key, key + data_length, other.key);
    return r.first == key + data_length;
}

void folder_info_t::add_relaxed(const file_info_ptr_t &file_info) noexcept {
    file_infos.put(file_info);
    auto seq = file_info->get_sequence();
    max_sequence = std::max(max_sequence, seq);
}

bool folder_info_t::add_strict(const file_info_ptr_t &file_info) noexcept {
    auto seq = file_info->get_sequence();
    if (seq > max_sequence) {
        max_sequence = seq;
        file_infos.put(file_info);
        return true;
    }
    return false;
}

void folder_info_t::serialize(db::FolderInfo &storage) const noexcept {
    db::set_index_id(storage, index);
    db::set_max_sequence(storage, max_sequence);
    db::set_introducer_device_key(storage, introducer_device_key);
}

utils::bytes_t folder_info_t::serialize() const noexcept {
    db::FolderInfo r;
    serialize(r);
    return db::encode(r);
}

void folder_info_t::set_index(std::uint64_t value) noexcept {
    if (value != this->index) {
        index = value;
        max_sequence = 0;
        file_infos.clear();
    }
}

bool folder_info_t::is_introduced_by(const model::device_id_t &device) const noexcept {
    return introducer_device_key == device.get_key();
}

utils::bytes_view_t folder_info_t::get_introducer_device_key() const noexcept { return introducer_device_key; }

folder_info_ptr_t folder_infos_map_t::by_uuid(utils::bytes_view_t uuid) const noexcept { return get<0>(uuid); }

folder_info_ptr_t folder_infos_map_t::by_device(const device_t &device) const noexcept {
    return get<1>(device.device_id().get_sha256());
}

folder_info_ptr_t folder_infos_map_t::by_device_id(utils::bytes_view_t device_id) const noexcept {
    return get<1>(device_id);
}

folder_info_ptr_t folder_infos_map_t::by_device_key(utils::bytes_view_t device_key) const noexcept {
    return get<2>(device_key);
}

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<0>(const folder_info_ptr_t &item) noexcept {
    return item->get_uuid();
}

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<1>(const folder_info_ptr_t &item) noexcept {
    return item->get_device()->device_id().get_sha256();
}

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<2>(const folder_info_ptr_t &item) noexcept {
    return item->get_device()->get_key();
}

} // namespace syncspirit::model


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

#pragma once

#include <cstdint>
#include "misc/augmentation.h"
#include "device.h"
#include "file_info.h"
#include "proto/proto-fwd.hpp"
#include "syncspirit-export.h"
#include "utils/bytes.h"
#include <boost/outcome.hpp>

namespace syncspirit::model {

namespace outcome = boost::outcome_v2;

struct folder_t;
using folder_ptr_t = intrusive_ptr_t<folder_t>;

struct folder_info_t;
using folder_info_ptr_t = intrusive_ptr_t<folder_info_t>;

struct SYNCSPIRIT_API folder_info_t final : augmentable_t {

    struct decomposed_key_t {
        static constexpr size_t folder_data_length = uuid_length + 1;

        decomposed_key_t(utils::bytes_view_t reduced_key, utils::bytes_view_t folder_uuid,
                         utils::bytes_view_t folder_info_id);
        decomposed_key_t(const decomposed_key_t &) = default;
        decomposed_key_t(decomposed_key_t &&) = default;

        inline utils::bytes_view_t const device_key() {
            return utils::bytes_view_t(device_key_raw, device_id_t::data_length);
        }
        inline utils::bytes_view_t const folder_key() {
            return utils::bytes_view_t(folder_key_raw, folder_data_length);
        }

        utils::bytes_view_t folder_info_id;
        unsigned char device_key_raw[device_id_t::data_length];
        unsigned char folder_key_raw[folder_data_length];
    };

    static decomposed_key_t decompose_key(utils::bytes_view_t key);

    static outcome::result<folder_info_ptr_t> create(utils::bytes_view_t key, const db::FolderInfo &data,
                                                     const device_ptr_t &device_, const folder_ptr_t &folder_) noexcept;
    static outcome::result<folder_info_ptr_t> create(const bu::uuid &uuid, const db::FolderInfo &data,
                                                     const device_ptr_t &device_, const folder_ptr_t &folder_) noexcept;
    utils::bytes_view_t get_key() const noexcept;
    utils::bytes_view_t get_uuid() const noexcept;

    bool operator==(const folder_info_t &other) const noexcept;
    bool operator!=(const folder_info_t &other) const noexcept { return !(*this == other); }

    void add_relaxed(const file_info_ptr_t &file_info) noexcept;
    bool add_strict(const file_info_ptr_t &file_info) noexcept;
    void serialize(db::FolderInfo &storage) const noexcept;
    utils::bytes_t serialize() const noexcept;

    inline std::uint64_t get_index() const noexcept { return index; }
    void set_index(std::uint64_t value) noexcept;

    inline device_t *get_device() const noexcept { return device; }
    inline folder_t *get_folder() const noexcept { return folder; }
    inline std::int64_t get_max_sequence() const noexcept { return max_sequence; }
    inline file_infos_map_t &get_file_infos() noexcept { return file_infos; }
    inline const file_infos_map_t &get_file_infos() const noexcept { return file_infos; }

    bool is_introduced_by(const model::device_id_t &device) const noexcept;
    utils::bytes_view_t get_introducer_device_key() const noexcept;

  private:
    folder_info_t(utils::bytes_view_t key, const device_ptr_t &device_, const folder_ptr_t &folder_) noexcept;
    folder_info_t(const bu::uuid &uuid, const device_ptr_t &device_, const folder_ptr_t &folder_) noexcept;
    void assign_fields(const db::FolderInfo &data) noexcept;

    static const constexpr auto data_length = uuid_length * 2 + device_id_t::digest_length + 1;

    unsigned char key[data_length];

    std::uint64_t index;
    std::int64_t max_sequence;
    utils::bytes_t introducer_device_key;
    device_t *device;
    folder_t *folder;
    file_infos_map_t file_infos;
};

using folder_info_ptr_t = intrusive_ptr_t<folder_info_t>;

struct SYNCSPIRIT_API folder_infos_map_t : public generic_map_t<folder_info_ptr_t, 3> {
    folder_info_ptr_t by_uuid(utils::bytes_view_t uuid) const noexcept;
    folder_info_ptr_t by_device(const device_t &device) const noexcept;
    folder_info_ptr_t by_device_id(utils::bytes_view_t device_id) const noexcept;
    folder_info_ptr_t by_device_key(utils::bytes_view_t device_id) const noexcept;
};

using uuid_folder_infos_map_t = std::unordered_map<utils::bytes_view_t, folder_info_t *>;

}; // namespace syncspirit::model


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

#include "ignored_device.h"

namespace syncspirit::model {

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<0>(const ignored_device_ptr_t &item) noexcept {
    return item->get_device_id().get_sha256();
}

} // namespace syncspirit::model


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

#pragma once

#include "some_device.h"
#include "db/prefix.h"

namespace syncspirit::model {

using ignored_device_t = some_device_t<(char)db::prefix::ignored_device>;
using ignored_devices_map_t = typename ignored_device_t::map_t;
using ignored_device_ptr_t = typename ignored_device_t::ptr_t;

}; // namespace syncspirit::model


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

#include "folder.h"
#include "ignored_folder.h"
#include "db/prefix.h"
#include "proto/proto-helpers.h"
#include "misc/error_code.h"

namespace syncspirit::model {

static const constexpr char prefix = (char)(db::prefix::ignored_folder);

outcome::result<ignored_folder_ptr_t> ignored_folder_t::create(std::string_view id, std::string_view label) noexcept {
    auto ptr = ignored_folder_ptr_t();
    ptr = new ignored_folder_t(id, label);
    return outcome::success(std::move(ptr));
}

outcome::result<ignored_folder_ptr_t> ignored_folder_t::create(utils::bytes_view_t key,
                                                               utils::bytes_view_t data) noexcept {
    if (key[0] != prefix) {
        return make_error_code(error_code_t::invalid_ignored_folder_prefix);
    }

    auto ptr = ignored_folder_ptr_t();
    ptr = new ignored_folder_t(key);

    auto r = ptr->assign_fields(data);
    if (!r) {
        return r.assume_error();
    }

    return outcome::success(std::move(ptr));
}

ignored_folder_t::ignored_folder_t(std::string_view folder_id, std::string_view label_) noexcept {
    key.resize(folder_id.size() + 1);
    key[0] = prefix;
    auto ptr = (unsigned char *)folder_id.data();
    std::copy(ptr, ptr + folder_id.size(), key.data() + 1);
    label = label_;
}

ignored_folder_t::ignored_folder_t(utils::bytes_view_t key_) noexcept {
    key = utils::bytes_t(key_.data(), key_.data() + key_.size());
}

outcome::result<void> ignored_folder_t::assign_fields(utils::bytes_view_t data) noexcept {
    db::IgnoredFolder folder;
    if (auto left = db::decode(data, folder); left) {
        return make_error_code(error_code_t::ignored_folder_deserialization_failure);
    }
    label = std::string(db::get_label(folder));
    return outcome::success();
}

utils::bytes_view_t ignored_folder_t::get_key() const noexcept { return key; }

utils::bytes_view_t ignored_folder_t::get_id() const noexcept { return {key.data() + 1, key.size() - 1}; }

std::string_view ignored_folder_t::get_label() const noexcept { return label; }

utils::bytes_t ignored_folder_t::serialize() noexcept {
    db::IgnoredFolder r;
    db::set_label(r, label);
    return db::encode(r);
}

template <> SYNCSPIRIT_API utils::bytes_view_t get_index<0>(const ignored_folder_ptr_t &item) noexcept {
    return item->get_id();
}

ignored_folder_ptr_t ignored_folders_map_t::by_key(utils::bytes_view_t key) const noexcept { return get<0>(key); }

} // namespace syncspirit::model


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

#pragma once

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

namespace syncspirit::model {

namespace outcome = boost::outcome_v2;

struct ignored_folder_t;

using ignored_folder_ptr_t = intrusive_ptr_t<ignored_folder_t>;

struct SYNCSPIRIT_API ignored_folder_t final : augmentable_t {
    static outcome::result<ignored_folder_ptr_t> create(std::string_view id, std::string_view label) noexcept;
    static outcome::result<ignored_folder_ptr_t> create(utils::bytes_view_t key, utils::bytes_view_t data) noexcept;

    utils::bytes_view_t get_key() const noexcept;
    utils::bytes_view_t get_id() const noexcept;
    std::string_view get_label() const noexcept;
    utils::bytes_t serialize() noexcept;

  private:
    ignored_folder_t(std::string_view folder_id, std::string_view label) noexcept;
    ignored_folder_t(utils::bytes_view_t key) noexcept;
    outcome::result<void> assign_fields(utils::bytes_view_t data) noexcept;

    std::string label;
    utils::bytes_t key;
};

struct SYNCSPIRIT_API ignored_folders_map_t : generic_map_t<ignored_folder_ptr_t, 1> {
    ignored_folder_ptr_t by_key(utils::bytes_view_t key) const noexcept;
};

} // namespace syncspirit::model


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

#pragma once

#include <rotor/request.hpp>
#include <boost/system/errc.hpp>
#include "cluster.h"
#include "diff/cluster_diff.h"

namespace syncspirit::model {

namespace r = rotor;

namespace payload {

struct model_response_t {
    model::cluster_ptr_t cluster;
};

struct model_request_t {
    using response_t = model_response_t;
};

struct model_update_t {
    model::diff::cluster_diff_ptr_t diff;
    const void *custom;
};

struct ui_ready_t {};

} // namespace payload

namespace message {

using model_update_t = r::message_t<payload::model_update_t>;
using ui_ready_t = r::message_t<payload::ui_ready_t>;

using model_request_t = r::request_traits_t<payload::model_request_t>::request::message_t;
using model_response_t = r::request_traits_t<payload::model_request_t>::response::message_t;

} // namespace message

} // namespace syncspirit::model


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

#pragma once

#include <boost/intrusive_ptr.hpp>
#include <boost/smart_ptr/intrusive_ref_counter.hpp>

namespace syncspirit::model {

template <typename T> struct arc_base_t : boost::intrusive_ref_counter<T, boost::thread_unsafe_counter> {
    using parent_t = boost::intrusive_ref_counter<T, boost::thread_unsafe_counter>;
    using parent_t::parent_t;
    arc_base_t(const arc_base_t &) = delete;
};

template <typename T> using intrusive_ptr_t = boost::intrusive_ptr<T>;

} // namespace syncspirit::model


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

#include "augmentation.h"

namespace syncspirit::model {

augmentable_t::~augmentable_t() {
    if (extension) {
        extension->on_delete();
    }
}

void augmentable_t::set_augmentation(augmentation_t &value) noexcept { extension = &value; }
void augmentable_t::set_augmentation(augmentation_ptr_t value) noexcept { extension = std::move(value); }

augmentation_ptr_t &augmentable_t::get_augmentation() noexcept { return extension; }

void augmentable_t::notify_update() noexcept {
    if (extension) {
        extension->on_update();
    }
}

} // namespace syncspirit::model


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

#pragma once

#include "arc.hpp"
#include "syncspirit-export.h"

#include <cstdint>

namespace syncspirit::model {

struct SYNCSPIRIT_API ref_countable_base_t {
    inline ref_countable_base_t() : counter{0} {}
    inline ref_countable_base_t(ref_countable_base_t &) : counter{0} {}
    inline virtual ~ref_countable_base_t() = default;
    inline std::uint32_t use_count() const noexcept { return counter; }

    mutable std::uint32_t counter;
};

struct SYNCSPIRIT_API ref_countable_t : virtual ref_countable_base_t {};

inline void intrusive_ptr_add_ref(const ref_countable_base_t *ptr) noexcept { ++ptr->counter; }

inline void intrusive_ptr_release(const ref_countable_base_t *ptr) noexcept {
    if (--ptr->counter == 0) {
        delete ptr;
    }
}

struct SYNCSPIRIT_API augmentation_t : ref_countable_t {
    augmentation_t() = default;
    inline virtual void on_update() noexcept {};
    inline virtual void on_delete() noexcept {};
};

using augmentation_ptr_t = intrusive_ptr_t<augmentation_t>;

struct SYNCSPIRIT_API augmentable_t : ref_countable_t {
    augmentable_t() = default;
    augmentable_t(const augmentable_t &) = delete;
    ~augmentable_t();

    void set_augmentation(augmentation_t &value) noexcept;
    void set_augmentation(augmentation_ptr_t value) noexcept;

    augmentation_ptr_t &get_augmentation() noexcept;

    void notify_update() noexcept;

  protected:
    augmentation_ptr_t extension;
};

} // namespace syncspirit::model


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

#include "model/file_info.h"
#include "block_iterator.h"
#include <limits>

using namespace syncspirit::model;

static constexpr auto wrong_index = std::numeric_limits<size_t>::max();

blocks_iterator_t::blocks_iterator_t(file_info_t &source_) noexcept : i{0}, source{&source_} {
    auto &sb = source->get_blocks();
    if (i == sb.size()) {
        i = wrong_index;
        return;
    }
    sequence = source->get_sequence();
    advance();
}

blocks_iterator_t::operator bool() const noexcept { return (i != wrong_index) && (source->get_sequence() == sequence); }

file_info_t *blocks_iterator_t::get_source() noexcept { return source.get(); }

void blocks_iterator_t::advance() noexcept {
    auto &sb = source->get_blocks();
    auto max = sb.size();
    while (i < max && sb[i]) {
        if (!source->is_locally_available(i)) {
            break;
        }
        ++i;
    }
    prepare();
}

void blocks_iterator_t::prepare() noexcept {
    if (i != wrong_index) {
        bool reset = source->is_unreachable() || (i >= source->blocks.size());
        if (reset) {
            i = wrong_index;
        }
    }
}

file_block_t blocks_iterator_t::next() noexcept {
    auto src = source.get();
    auto &sb = src->get_blocks();
    auto idx = i;
    ++i;
    advance();
    return {sb[idx].get(), src, idx};
}


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

#pragma once

#include <vector>
#include "arc.hpp"
#include "model/block_info.h"
#include "syncspirit-export.h"

namespace syncspirit::model {

struct folder_info_t;
struct file_info_t;

struct SYNCSPIRIT_API blocks_iterator_t : arc_base_t<blocks_iterator_t> {
    using blocks_t = std::vector<block_info_ptr_t>;

    blocks_iterator_t(file_info_t &source) noexcept;
    operator bool() const noexcept;

    file_block_t next() noexcept;
    file_info_t *get_source() noexcept;

  private:
    void prepare() noexcept;
    void advance() noexcept;
    size_t i = 0;
    std::int64_t sequence;
    file_info_ptr_t source;
};

using block_iterator_ptr_t = intrusive_ptr_t<blocks_iterator_t>;

} // namespace syncspirit::model


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

#include "error_code.h"

namespace syncspirit::model {

namespace detail {

const char *error_code_category_t::name() const noexcept { return "model error"; }

std::string error_code_category_t::message(int c) const {
    std::string r;
    switch (static_cast<error_code_t>(c)) {
    case error_code_t::success:
        r = "success";
        break;
    case error_code_t::unknown_device:
        r = "peer device is unknown";
        break;
    case error_code_t::no_such_device:
        r = "no such device";
        break;
    case error_code_t::device_already_exists:
        r = "device already exists";
        break;
    case error_code_t::no_such_folder:
        r = "no such folder";
        break;
    case error_code_t::invalid_block_key_length:
        r = "invalid block key length";
        break;
    case error_code_t::invalid_block_prefix:
        r = "invalid block key prefix";
        break;
    case error_code_t::block_deserialization_failure:
        r = "block deserialization failure";
        break;
    case error_code_t::invalid_device_sha256_digest:
        r = "invalid device sha256 digest";
        break;
    case error_code_t::invalid_device_prefix:
        r = "invalid device key prefix";
        break;
    case error_code_t::device_deserialization_failure:
        r = "device deserialization failure";
        break;
    case error_code_t::invalid_folder_key_length:
        r = "invalid folder key length";
        break;
    case error_code_t::invalid_folder_prefix:
        r = "invalid folder prefix";
        break;
    case error_code_t::invalid_pending_folder_length:
        r = "invalid pending folder prefix";
        break;
    case error_code_t::folder_deserialization_failure:
        r = "folder deserialization failure";
        break;
    case error_code_t::pending_folder_deserialization_failure:
        r = "pending folder deserialization failure";
        break;
    case error_code_t::file_info_deserialization_failure:
        r = "file info deserialization failure";
        break;
    case error_code_t::invalid_file_info_key_length:
        r = "invalid file info key length";
        break;
    case error_code_t::invalid_file_info_prefix:
        r = "invalid file info prefix";
        break;
    case error_code_t::invalid_folder_info_key_length:
        r = "invalid folder info key length prefix";
        break;
    case error_code_t::invalid_folder_info_prefix:
        r = "invalid folder info prefix";
        break;
    case error_code_t::folder_info_deserialization_failure:
        r = "folder info deserialization failure";
        break;
    case error_code_t::invalid_some_device_key_length:
        r = "invalid some device key length";
        break;
    case error_code_t::invalid_some_device_prefix:
        r = "invalid some device prefix";
        break;
    case error_code_t::some_device_deserialization_failure:
        r = "some device deserialization failure";
        break;
    case error_code_t::invalid_ignored_folder_prefix:
        r = "invalid ignored folder prefix";
        break;
    case error_code_t::ignored_folder_deserialization_failure:
        r = "ignored folder deserialization failure";
        break;
    case error_code_t::cannot_remove_self:
        r = "cannot remove self";
        break;
    case error_code_t::source_device_not_exists:
        r = "source device does not exist";
        break;
    case error_code_t::folder_does_not_exist:
        r = "folder does not exist";
        break;
    case error_code_t::folder_is_already_shared:
        r = "folder is already shared";
        break;
    case error_code_t::folder_is_not_shared:
        r = "folder is not shared";
        break;
    case error_code_t::malformed_deviceid:
        r = "device id is malformed";
        break;
    case error_code_t::invalid_block_size:
        r = "block size is invalid (i.e. greater than file size)";
        break;
    case error_code_t::unexpected_blocks:
        r = "blocks are not expected (e.g. in deleted file)";
        break;
    case error_code_t::missing_version:
        r = "file version is missing";
        break;
    case error_code_t::mismatch_file_size:
        r = "file size mismatches total size of it's blocks";
        break;
    case error_code_t::invalid_sequence:
        r = "file sequence is invalid";
        break;
    case error_code_t::empty_folder_name:
        r = "folder name is empty";
        break;
    default:
        r = "unknown";
        break;
    }

    r += " (";
    r += std::to_string(c);
    r += ")";

    return r;
}

} // namespace detail

const static detail::error_code_category_t category;

const detail::error_code_category_t &error_code_category() { return category; }

} // namespace syncspirit::model


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

#pragma once

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

namespace syncspirit::model {

enum class error_code_t {
    success = 0,
    unknown_device,
    no_such_device,
    device_already_exists,
    no_such_folder,
    invalid_block_prefix,
    invalid_block_key_length,
    block_deserialization_failure,
    invalid_device_prefix,
    invalid_device_sha256_digest,
    device_deserialization_failure,
    invalid_folder_key_length,
    invalid_folder_prefix,
    invalid_pending_folder_length,
    folder_deserialization_failure,
    pending_folder_deserialization_failure,
    file_info_deserialization_failure,
    invalid_file_info_key_length,
    invalid_file_info_prefix,
    invalid_folder_info_key_length,
    invalid_folder_info_prefix,
    folder_info_deserialization_failure,
    invalid_some_device_key_length,
    invalid_some_device_prefix,
    some_device_deserialization_failure,
    invalid_ignored_folder_prefix,
    ignored_folder_deserialization_failure,
    cannot_remove_self,
    source_device_not_exists,
    folder_does_not_exist,
    folder_is_already_shared,
    malformed_deviceid,
    folder_is_not_shared,
    invalid_block_size,
    unexpected_blocks,
    missing_version,
    mismatch_file_size,
    invalid_sequence,
    empty_folder_name,
};

namespace detail {

class SYNCSPIRIT_API error_code_category_t : 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::error_code_category_t &error_code_category();

inline boost::system::error_code make_error_code(error_code_t e) {
    return {static_cast<int>(e), error_code_category()};
}

} // namespace syncspirit::model

namespace boost {
namespace system {

template <> struct is_error_code_enum<syncspirit::model::error_code_t> : std::true_type {
    static const bool value = true;
};

} // namespace system
} // namespace boost


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

#include "file_block.h"
#include "../block_info.h"
#include "../file_info.h"

namespace syncspirit::model {

file_block_t::file_block_t(block_info_t *block, file_info_t *file_info_, std::size_t block_index_) noexcept
    : block_info{block}, file_info{file_info_}, block_idx{block_index_}, local{block->local_file()} {}

bool file_block_t::matches(const block_info_t *block, const file_info_t *file) noexcept {
    return block_info == block && file_info == file;
}

file_block_t &file_block_t::operator=(const file_block_t &other) noexcept {
    block_info = other.block_info;
    file_info = other.file_info;
    block_idx = other.block_idx;
    local = other.local;
    return *this;
}

file_block_t::operator bool() const noexcept { return (bool)file_info; }

size_t file_block_t::get_offset() const noexcept {
    assert(file_info);
    return file_info->get_block_offset(block_idx);
}

} // namespace syncspirit::model


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

#pragma once

#include "arc.hpp"
#include "syncspirit-export.h"

namespace syncspirit::model {

struct block_info_t;
struct file_info_t;
using block_info_ptr_t = intrusive_ptr_t<block_info_t>;

struct SYNCSPIRIT_API file_block_t {
    file_block_t() noexcept = default;
    file_block_t(file_block_t &&) noexcept = default;
    file_block_t(const file_block_t &) noexcept = default;
    file_block_t(block_info_t *block, file_info_t *file_info, std::size_t block_index) noexcept;

    file_block_t &operator=(const file_block_t &) noexcept;

    bool matches(const block_info_t *, const file_info_t *) noexcept;
    explicit operator bool() const noexcept;
    block_info_t *block() const noexcept { return block_info; }
    inline void mark_locally_available(bool value = true) noexcept { local = value; }
    inline bool is_locally_available() const noexcept { return local; }
    inline file_info_t *file() noexcept { return file_info; }
    inline const file_info_t *file() const noexcept { return file_info; }

    inline size_t block_index() const noexcept { return block_idx; }
    size_t get_offset() const noexcept;

  private:
    block_info_t *block_info;
    file_info_t *file_info;
    std::size_t block_idx;
    bool local = false;
};

} // namespace syncspirit::model


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

#include "file_iterator.h"
#include "resolver.h"
#include "model/cluster.h"

#include <spdlog/spdlog.h>

using namespace syncspirit::model;

bool file_iterator_t::file_comparator_t::operator()(const file_info_t *l, const file_info_t *r) const {
    using P = db::PullOrder;

    auto le = l->get_blocks().empty();
    auto re = r->get_blocks().empty();

    if (le && !re) {
        return true;
    } else if (re && !le) {
        return false;
    }

    auto cmp = std::strong_ordering::equal;
    if (pull_order == P::newest) {
        cmp = r->get_modified_s() <=> l->get_modified_s();
    } else if (pull_order == P::oldest) {
        cmp = l->get_modified_s() <=> r->get_modified_s();
    } else if (pull_order == P::smallest) {
        cmp = l->get_size() <=> r->get_size();
    } else if (pull_order == P::largest) {
        cmp = r->get_size() <=> l->get_size();
    }

    if (cmp == std::strong_ordering::less) {
        return true;
    } else if (cmp == std::strong_ordering::greater) {
        return false;
    }

    auto ln = l->get_name();
    auto rn = r->get_name();

    return std::lexicographical_compare(ln.begin(), ln.end(), rn.begin(), rn.end());
}

file_iterator_t::file_iterator_t(cluster_t &cluster_, const device_ptr_t &peer_) noexcept
    : cluster{cluster_}, peer{peer_.get()}, folder_index{0} {
    auto &folders = cluster.get_folders();

    for (auto &[folder, _] : folders) {
        auto peer_folder = folder->get_folder_infos().by_device(*peer);
        if (!peer_folder) {
            continue;
        }
        auto my_folder = folder->get_folder_infos().by_device(*cluster.get_device());
        if (!my_folder) {
            continue;
        }
        prepare_folder(std::move(peer_folder));
    }
}

auto file_iterator_t::find_folder(folder_t *folder) noexcept -> folder_iterator_t & {
    auto predicate = [&](auto &it) -> bool { return it.peer_folder->get_folder() == folder; };
    auto it = std::find_if(folders_list.begin(), folders_list.end(), predicate);
    return *it;
}

auto file_iterator_t::prepare_folder(folder_info_ptr_t peer_folder) noexcept -> folder_iterator_t & {
    auto &files = peer_folder->get_file_infos();
    auto folder = peer_folder->get_folder();
    auto order = folder->get_pull_order();
    auto set = std::make_unique<queue_t>(file_comparator_t{order});
    auto seen_sequence = std::int64_t{0};
    bool can_receive = folder->get_folder_type() != db::FolderType::send;

    if (can_receive) {
        for (auto it : files) {
            auto f = it.item.get();
            if (resolve(*f) != advance_action_t::ignore) {
                set->emplace(f);
            }
        }
        seen_sequence = peer_folder->get_max_sequence();
    }

    auto it = set->begin();
    folders_list.emplace_back(folder_iterator_t{peer_folder, std::move(set), seen_sequence, it, can_receive});
    return folders_list.back();
}

auto file_iterator_t::next() noexcept -> result_t {
    auto folders_count = folders_list.size();
    auto folder_scans = size_t{0};

    while (folder_scans < folders_count) {
        auto &fi = folders_list[folder_index];
        auto &file_infos = fi.peer_folder->get_folder()->get_folder_infos();
        auto local_folder = file_infos.by_device(*cluster.get_device());

        auto &queue = fi.files_queue;
        auto folder = local_folder->get_folder();
        auto do_scan = !folder->is_paused() && !folder->is_scheduled() && !folder->is_suspended() && !queue->empty();

        // check other files
        if (do_scan) {
            auto it = queue->begin();
            while (it != queue->end()) {
                auto &file = **it;
                it = queue->erase(it);
                auto action = resolve(file);
                if (action != advance_action_t::ignore) {
                    return std::make_pair(&file, action);
                }
            }
        }
        folder_index = (folder_index + 1) % folders_count;
        ++folder_scans;
    }
    return {nullptr, advance_action_t::ignore};
}

void file_iterator_t::on_upsert(folder_info_ptr_t peer_folder) noexcept {
    auto folder = peer_folder->get_folder();
    for (auto &it : folders_list) {
        if (it.peer_folder->get_folder() == folder && it.can_receive) {
            return populate(it);
        }
    }
    prepare_folder(peer_folder);
}

void file_iterator_t::populate(folder_iterator_t &it) noexcept {
    auto peer_folder = it.peer_folder.get();
    auto &files_map = peer_folder->get_file_infos();
    auto seen_sequence = it.seen_sequence;
    auto max_sequence = peer_folder->get_max_sequence();
    auto [from, to] = files_map.range(seen_sequence + 1, max_sequence);
    for (auto fit = from; fit != to; ++fit) {
        auto file = fit->item.get();
        if (resolve(*file) != advance_action_t::ignore) {
            it.files_queue->insert(file);
        }
    }
    it.seen_sequence = max_sequence;
    it.it = it.files_queue->begin();
}

void file_iterator_t::on_upsert(folder_t &folder) noexcept {
    auto order = folder.get_pull_order();
    for (auto &it : folders_list) {
        if (it.peer_folder->get_folder() == &folder) {
            auto &peer_folder = *it.peer_folder;
            auto folder_type = peer_folder.get_folder()->get_folder_type();
            auto can_receive = folder_type != db::FolderType::send;
            if (can_receive) {
                if (!it.can_receive) {
                    it.seen_sequence = 0;
                    populate(it);
                } else if (it.files_queue->key_comp().pull_order != order) {
                    auto new_set = std::make_unique<queue_t>(file_comparator_t{order});
                    for (auto fi : *it.files_queue) {
                        new_set->insert(fi);
                    }
                    it.files_queue = std::move(new_set);
                    it.it = it.files_queue->begin();
                }
            } else {
                if (it.can_receive) {
                    it.files_queue->clear();
                    it.it = it.files_queue->begin();
                }
            }
            it.can_receive = can_receive;
        }
    }
}

void file_iterator_t::on_remove(folder_info_ptr_t peer_folder) noexcept {
    for (auto it = folders_list.begin(); it != folders_list.end(); ++it) {
        if (it->peer_folder == peer_folder) {
            folders_list.erase(it);
            folder_index = 0;
            return;
        }
    }
}

void file_iterator_t::recheck(file_info_t &remote) noexcept {
    for (auto &fi : folders_list) {
        if (fi.peer_folder.get() == remote.get_folder_info()) {
            if (fi.can_receive) {
                if (resolve(remote) != advance_action_t::ignore) {
                    fi.files_queue->emplace(&remote);
                }
            }
            break;
        }
    }
}


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

#pragma once

#include "resolver.h"
#include "model/device.h"
#include "model/file_info.h"
#include "model/folder_info.h"
#include "model/folder.h"
#include "syncspirit-export.h"
#include <vector>
#include <set>
#include <memory>
#include <utility>

namespace syncspirit::model {

using compare_fn_t = bool(const file_info_t *l, const file_info_t *r);

struct SYNCSPIRIT_API file_iterator_t : arc_base_t<file_iterator_t> {
    using files_list_t = std::vector<file_info_ptr_t>;
    using result_t = std::pair<file_info_t *, advance_action_t>;

    file_iterator_t(cluster_t &cluster, const device_ptr_t &peer) noexcept;
    file_iterator_t(const file_iterator_t &) = delete;

    result_t next() noexcept;

    void on_upsert(folder_t &folder) noexcept;
    void on_upsert(folder_info_ptr_t peer_folder) noexcept;
    void on_remove(folder_info_ptr_t peer_folder) noexcept;
    void recheck(file_info_t &file) noexcept;

  private:
    struct file_comparator_t {
        bool operator()(const file_info_t *l, const file_info_t *r) const;
        db::PullOrder pull_order;
    };
    using file_comparator_ptr_t = std::unique_ptr<file_comparator_t>;
    using queue_t = std::set<file_info_t *, file_comparator_t>;
    using queue_ptr_t = std::unique_ptr<queue_t>;

    struct folder_iterator_t {
        using it_t = typename queue_t::iterator;
        model::folder_info_ptr_t peer_folder;
        queue_ptr_t files_queue;
        std::int64_t seen_sequence;
        it_t it;
        bool can_receive;
    };
    using folder_iterators_t = std::vector<folder_iterator_t>;

    folder_iterator_t &prepare_folder(folder_info_ptr_t peer_folder) noexcept;
    folder_iterator_t &find_folder(folder_t *folder) noexcept;
    void populate(folder_iterator_t &it) noexcept;

    cluster_t &cluster;
    device_t *peer;
    std::size_t folder_index;
    folder_iterators_t folders_list;
};

using file_iterator_ptr_t = intrusive_ptr_t<file_iterator_t>;

} // namespace syncspirit::model


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

#pragma once
#include <string>
#include <boost/multi_index/global_fun.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index_container.hpp>

namespace syncspirit::model {

namespace details {

namespace mi = boost::multi_index;

template <typename T> std::string_view get_lru_key(const T &key);

// https://www.boost.org/doc/libs/1_45_0/libs/multi_index/example/serialization.cpp
template <typename Item> class mru_list_t {

    struct tag_seq {};
    struct tag_hash {};

    using item_list_t = mi::multi_index_container<
        Item, mi::indexed_by<mi::sequenced<mi::tag<tag_seq>>,
                             mi::hashed_unique<mi::tag<tag_hash>,
                                               mi::global_fun<const Item &, std::string_view, &get_lru_key<Item>>>>>;

  public:
    using item_t = Item;

    mru_list_t(size_t max_items_) : max_items(max_items_) {}

    void put(const Item &item) noexcept {
        auto p = il.push_front(item);

        if (!p.second) {                      /* duplicate item */
            il.relocate(il.begin(), p.first); /* put in front */
        } else if (il.size() > max_items) {   /* keep the length <= max_num_items */
            il.pop_back();
        }
    }

    void remove(const Item &item) noexcept {
        auto &projection = il.template get<1>();
        auto key = get_lru_key(item);
        auto it = projection.find(key);
        if (it != projection.end()) {
            auto it_0 = il.template project<tag_seq>(it);
            il.erase(it_0);
        }
    }

    item_t get(const std::string &key) noexcept {
        auto &projection = il.template get<1>();
        auto it = projection.find(key);
        if (it != projection.end()) {
            auto it_0 = il.template project<tag_seq>(it);
            il.relocate(il.begin(), it_0);
            return *it;
        }
        return {};
    }

    void clear() noexcept { il.clear(); }

  private:
    item_list_t il;
    std::size_t max_items;
};

} // namespace details

template <typename T> using mru_list_t = details::mru_list_t<T>;

} // namespace syncspirit::model


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

#pragma once

#include <tuple>
#include <utility>
#include "utils/bytes.h"
#include <boost/multi_index/global_fun.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index_container.hpp>

namespace syncspirit::model {

namespace details {

namespace mi = boost::multi_index;

template <size_t N> struct string_tuple_t;
template <> struct string_tuple_t<1> {
    using key_storage_t = std::tuple<utils::bytes_t>;
};
template <> struct string_tuple_t<2> {
    using key_storage_t = std::tuple<utils::bytes_t, utils::bytes_t>;
};
template <> struct string_tuple_t<3> {
    using key_storage_t = std::tuple<utils::bytes_t, utils::bytes_t, utils::bytes_t>;
};

template <typename Item, size_t N> struct indexed_item_t {
    using storage_t = typename string_tuple_t<N>::key_storage_t;
    using item_t = Item;
    static constexpr size_t size = N;
    item_t item;
    mutable storage_t keys;

    indexed_item_t(const item_t &item_, const storage_t &keys_) noexcept : item{item_}, keys{keys_} {}

    template <size_t I> auto &get() noexcept { return std::get<I>(keys); }
};

template <typename T> struct reduced_type_t {
    using type = T;
};

template <> struct reduced_type_t<utils::bytes_t> {
    using type = utils::bytes_view_t;
};

template <typename Key, size_t I>
using single_key_t = reduced_type_t<std::tuple_element_t<I, typename Key::storage_t>>::type;

} // namespace details

// template <size_t I, typename T> std::string_view get_index(const T &item) noexcept;
template <size_t I, typename Result, typename T> Result get_index(const T &item) noexcept;

namespace details {

template <size_t I, typename K> single_key_t<K, I> get_key(const K &key) noexcept {
    return get_index<I, single_key_t<K, I>>(key.item);
}

struct hash_op_t {
    std::size_t operator()(utils::bytes_view_t v) const noexcept { return std::hash<utils::bytes_view_t>()(v); }
    std::size_t operator()(const utils::bytes_t &s) const noexcept { return std::hash<utils::bytes_t>()(s); }
    std::size_t operator()(std::int64_t v) const noexcept { return std::hash<std::int64_t>()(v); }
};

struct eq_op_t {
    bool operator()(const utils::bytes_t &s1, const utils::bytes_t &s2) const noexcept { return s1 == s2; }
    bool operator()(const utils::bytes_t &s1, utils::bytes_view_t s2) const noexcept { return s1 == s2; }
    bool operator()(utils::bytes_view_t s1, const utils::bytes_t &s2) const { return s1 == s2; }
    bool operator()(utils::bytes_view_t s1, utils::bytes_view_t s2) const { return s1 == s2; }
    bool operator()(std::int64_t v1, std::int64_t v2) const { return v1 == v2; }
};

template <size_t I, typename K> struct indexed_by {
    using type = mi::hashed_unique<mi::global_fun<const K &, single_key_t<K, I>, &get_key<I, K>>, hash_op_t, eq_op_t>;
};

template <size_t N, size_t I, typename Key> struct hash_impl_t {
    using type = indexed_by<I, Key>::type;
};

template <size_t I, size_t N, typename K, typename... Ks> struct hasher_t {
    using type = typename hasher_t<I - 1, N, K, typename hash_impl_t<N, I - 1, K>::type, Ks...>::type;
};

template <size_t N, typename K, typename... Ks> struct hasher_t<0, N, K, Ks...> {
    using type = mi::indexed_by<Ks...>;
};

template <typename T, size_t N = 1>
using unordered_string_map_t =
    mi::multi_index_container<indexed_item_t<T, N>, typename hasher_t<N, N, indexed_item_t<T, N>>::type>;

template <typename Item, typename WrappedItem> struct generalized_map_t {
    static constexpr size_t N = WrappedItem::size;
    using map_t = details::unordered_string_map_t<Item, N>;
    using composite_key_t = typename WrappedItem::storage_t;
    using iterator_t = decltype(std::declval<map_t>().template get<0>().begin());
    using const_iterator_t = decltype(std::declval<map_t>().template get<0>().cbegin());
    using sequence_t = std::make_index_sequence<N>;

    void put(const Item &item) noexcept {
        auto r = key2item.emplace(item, make_key(item));
        if (!r.second) {
            remove(item);
            r = key2item.emplace(item, make_key(item));
            assert(r.second);
        }
    }

    void remove(const Item &item) noexcept {
        [&]<std::size_t... I>(const Item &item, std::index_sequence<I...>) {
            (key2item.template get<I>().erase(get_index<I, details::single_key_t<WrappedItem, I>>(item)), ...);
        }(item, sequence_t{});
    }

    template <size_t I = 0> Item get(details::single_key_t<WrappedItem, I> id) const noexcept {
        auto &projection = key2item.template get<I>();
        auto it = projection.find(id);
        if (it != projection.end()) {
            return (*it).item;
        }
        return {};
    }

    size_t size() const noexcept { return key2item.size(); }

    iterator_t begin() noexcept { return key2item.template get<0>().begin(); }

    iterator_t end() noexcept { return key2item.template get<0>().end(); }

    const_iterator_t begin() const noexcept { return key2item.template get<0>().cbegin(); }

    const_iterator_t end() const noexcept { return key2item.template get<0>().cend(); }

    bool operator==(const generalized_map_t &other) const noexcept {
        auto &keys = key2item.template get<0>();
        auto &other_keys = other.key2item.template get<0>();
        if (keys.size() == other.size()) {
            for (auto &it : *this) {
                auto &key = std::get<0>(it.keys);
                auto other_it = other_keys.find(key);
                if (other_it == other_keys.end()) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    void clear() noexcept { key2item.clear(); }

  protected:
    inline composite_key_t make_key(const Item &item) {
        return []<std::size_t... I>(const Item &item, std::index_sequence<I...>) -> composite_key_t {
            return composite_key_t{get_index<I, details::single_key_t<WrappedItem, I>>(item)...};
        }(item, sequence_t{});
    }
    map_t key2item;
};

} // namespace details

template <typename Item, size_t N>
using generic_map_t = details::generalized_map_t<Item, details::indexed_item_t<Item, N>>;

}; // namespace syncspirit::model


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

#include "orphaned_blocks.h"
#include "model/file_info.h"

using namespace syncspirit::model;

void orphaned_blocks_t::record(file_info_t &file) { file_for_removal.emplace(&file); }

auto orphaned_blocks_t::deduce() const -> set_t {
    using set_view_t = std::set<utils::bytes_view_t, utils::bytes_comparator_t>;
    set_view_t processed;
    set_t r;
    for (auto &file : file_for_removal) {
        auto &blocks = file->get_blocks();
        for (auto &b : blocks) {
            auto key = b->get_key();
            if (processed.contains(b->get_key())) {
                continue;
            }
            auto &file_blocks = b->get_file_blocks();
            auto usages = file_blocks.size();
            for (auto &fb : file_blocks) {
                auto target_file = file_info_ptr_t(fb.file());
                auto it = file_for_removal.find(target_file);
                if (it != file_for_removal.end()) {
                    --usages;
                }
            }
            if (!usages) {
                auto copy = utils::bytes_t(key.begin(), key.end());
                r.emplace(std::move(copy));
            }
            processed.emplace(key);
        }
    }
    return r;
}


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

#pragma once

#include "model/file_info.h"
#include "utils/bytes_comparator.hpp"
#include "utils/bytes.h"
#include <set>

namespace syncspirit::model {

struct SYNCSPIRIT_API orphaned_blocks_t {
    using set_t = std::set<utils::bytes_t, utils::bytes_comparator_t>;

    void record(file_info_t &);
    set_t deduce() const;

    file_infos_set_t file_for_removal;
};

} // namespace syncspirit::model


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

#pragma once

#include "augmentation.h"
#include "syncspirit-export.h"

namespace syncspirit::model {

struct SYNCSPIRIT_API proxy_t : model::augmentable_t, model::augmentation_t {};

} // namespace syncspirit::model