diff --git a/source/cdcdb/cdc/cas.d b/source/cdcdb/cdc/cas.d index 023c139..d3e8034 100644 --- a/source/cdcdb/cdc/cas.d +++ b/source/cdcdb/cdc/cas.d @@ -3,6 +3,10 @@ module cdcdb.cdc.cas; import cdcdb.db; import cdcdb.cdc.core; +import std.digest.sha : SHA256, digest; +import std.format : format; + +// CAS-хранилище (Content-Addressable Storage) со снапшотами final class CAS { private: @@ -13,8 +17,13 @@ public: _db = new DBLite(database); } - size_t saveSnapshot(const(ubyte)[] data) + size_t saveSnapshot(string filePath, const(ubyte)[] data) { + ubyte[32] hashSource = digest!SHA256(data); + // Сделать запрос в БД по filePath и сверить хеш файлов + + + // Параметры для CDC вынести в отдельные настройки (продумать) auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F); // Разбить на фрагменты @@ -26,7 +35,7 @@ public: // Записать фрагменты в БД foreach (chunk; chunks) { - writeln(chunk.index); + writeln(format("%(%02x%)", chunk.sha256)); } _db.commit(); // Записать манифест в БД diff --git a/source/cdcdb/cdc/core.d b/source/cdcdb/cdc/core.d index f09732c..3412113 100644 --- a/source/cdcdb/cdc/core.d +++ b/source/cdcdb/cdc/core.d @@ -4,6 +4,7 @@ import cdcdb.cdc.types; import std.digest.sha : SHA256, digest; +// Change Data Capture final class CDC { private: diff --git a/source/cdcdb/cdc/types.d b/source/cdcdb/cdc/types.d index c3a41f6..2120893 100644 --- a/source/cdcdb/cdc/types.d +++ b/source/cdcdb/cdc/types.d @@ -1,6 +1,6 @@ module cdcdb.cdc.types; -/// Единица разбиения +// Единица разбиения struct Chunk { size_t index; // 1..N @@ -9,7 +9,7 @@ struct Chunk ubyte[32] sha256; // hex(SHA-256) содержимого } -/// Метаданные снимка +// Метаданные снимка struct SnapshotInfo { size_t id; diff --git a/source/cdcdb/db/dblite.d b/source/cdcdb/db/dblite.d index 184238c..3bba325 100644 --- a/source/cdcdb/db/dblite.d +++ b/source/cdcdb/db/dblite.d @@ -9,6 +9,11 @@ private: string _dbPath; // _scheme mixin(import("scheme.d")); + + SqliteResult sql(T...)(string queryText, T args) + { + return cast(SqliteResult) query(queryText, args); + } public: this(string database) { @@ -40,8 +45,5 @@ public: query("ROLLBACK"); } - SqliteResult sql(T...)(string queryText, T args) - { - return cast(SqliteResult) query(queryText, args); - } + // findFile() } diff --git a/source/cdcdb/db/scheme.d b/source/cdcdb/db/scheme.d index 557e0cf..adabd60 100644 --- a/source/cdcdb/db/scheme.d +++ b/source/cdcdb/db/scheme.d @@ -1,75 +1,202 @@ auto _scheme = [ q{ - -- Метаданные снапшота + -- ------------------------------------------------------------ + -- Метаданные снимка + -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS snapshots ( -- Уникальный числовой идентификатор снимка. Используется во внешних ключах. id INTEGER PRIMARY KEY AUTOINCREMENT, - -- Произвольная метка/название снимка. + + -- Путь к исходному файлу, для удобства навигации/поиска. + file_path TEXT, + + -- Произвольная метка/название снимка (для человека). label TEXT, - -- Время создания записи в UTC. По умолчанию - сейчас. + + -- Время создания записи (UTC). По умолчанию - текущее. created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), - -- Полная длина исходного файла в байтах для этого снимка (до разбиения на чанки). + + -- Полная длина исходного файла в байтах (до разбиения на чанки). source_length INTEGER NOT NULL, - -- Пороговые размеры FastCDC (минимальный/целевой/максимальный размер чанка) в байтах. - -- Фиксируются здесь, чтобы позже можно было корректно пересобрать/сравнить. + + -- Пороговые размеры FastCDC: минимальный/целевой/максимальный размер чанка (в байтах). + -- Фиксируются для воспроизводимости и сравнения результатов. algo_min INTEGER NOT NULL, algo_normal INTEGER NOT NULL, algo_max INTEGER NOT NULL, - -- Маски для определения границ чанков (быстрый роллинг-хэш/FastCDC). - -- Обычно степени вида 2^n - 1. Хранятся для воспроизводимости. + + -- Маски FastCDC для определения границ чанков (обычно вида 2^n - 1). + -- Хранятся для воспроизводимости. mask_s INTEGER NOT NULL, mask_l INTEGER NOT NULL, + -- Состояние снимка: - -- pending - метаданные созданы, состав не полностью загружен; - -- ready - все чанки привязаны, снимок готов к использованию. + -- "pending" - метаданные созданы, состав не полностью загружен; + -- "ready" - все чанки привязаны, снимок готов к использованию. status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready")) ) }, q{ - -- Уникальные куски содержимого (сам контент в БД) + -- ------------------------------------------------------------ + -- Уникальные куски содержимого (дедупликация по sha256) + -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS blobs ( - -- Хэш содержимого чанка. Ключ обеспечивает дедупликацию: одинаковый контент хранится один раз. - sha256 TEXT PRIMARY KEY, - -- Размер этого чанка в байтах. + -- Хэш содержимого чанка. Обеспечивает уникальность контента. + -- Храним как BLOB(32) (сырые 32 байта SHA-256), а не hex-строку. + sha256 BLOB PRIMARY KEY CHECK (length(sha256) = 32), + + -- Размер чанка в байтах. Должен совпадать с длиной content. size INTEGER NOT NULL, + -- Сырые байты чанка. content BLOB NOT NULL, - -- Когда этот контент впервые появился в базе (UTC). - created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) + + -- Момент, когда этот контент впервые появился в базе (UTC). + created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + + -- Последний раз, когда на контент сослались (для аналитики/GC). + last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + + -- Счётчик ссылок: сколько строк в snapshot_chunks ссылаются на этот sha256. + -- Используется для безопасного удаления неиспользуемых blob-ов. + refcount INTEGER NOT NULL DEFAULT 0, + + -- Дополнительные гарантии целостности: + CHECK (refcount >= 0), + CHECK (size = length(content)) ) }, q{ - -- Состав снапшота (порядок чанков важен) + -- ------------------------------------------------------------ + -- Состав снимка (упорядоченный список чанков) + -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS snapshot_chunks ( -- Ссылка на snapshots.id. Определяет, к какому снимку относится строка. snapshot_id INTEGER NOT NULL, - -- Позиция чанка в снимке (индексация). + + -- Позиция чанка в снимке (индексация на твой выбор - 0/1-based; важно быть последовательным). -- Обеспечивает порядок сборки. chunk_index INTEGER NOT NULL, - -- Смещение чанка в исходном файле в байтах. - -- Можно восстановить как сумму size предыдущих чанков по chunk_index, - -- но хранение ускоряет проверки/отладку. + + -- Смещение чанка в исходном файле (в байтах). + -- Можно восстановить суммой size предыдущих чанков, но хранение ускоряет проверки/отладку. offset INTEGER, - -- Размер именно этого чанка в байтах (дублирует blobs.size для быстрого доступа и валидации). + + -- Размер конкретного чанка в составе (дублирует blobs.size для ускорения/валидации). size INTEGER NOT NULL, - -- Ссылка на blobs.sha256. Привязывает позицию в снимке к конкретному содержимому. - sha256 TEXT NOT NULL, - -- Гарантирует уникальность позиции чанка в рамках снимка и задаёт естественный порядок. + + -- Ссылка на blobs.sha256. Привязывает позицию к конкретному содержимому. + -- Тип BLOB обязан совпадать с типом в родительской таблице. + sha256 BLOB NOT NULL, + + -- Уникальность позиции чанка в рамках одного снимка. PRIMARY KEY (snapshot_id, chunk_index), + + -- Внешние ключи и их поведение: -- При удалении снимка его строки состава удаляются автоматически. - -- Обновления id каскадятся (на практике id не меняют). - FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON UPDATE CASCADE ON DELETE CASCADE, - -- Нельзя удалить blob, если он где-то используется (RESTRICT). - -- Обновление хэша каскадится (редкий случай). - FOREIGN KEY (sha256) REFERENCES blobs(sha256) ON UPDATE CASCADE ON DELETE RESTRICT + FOREIGN KEY (snapshot_id) + REFERENCES snapshots(id) + ON UPDATE CASCADE + ON DELETE CASCADE, + + -- Нельзя удалить blob, если он где-то используется; обновление ключа запрещено. + FOREIGN KEY (sha256) + REFERENCES blobs(sha256) + ON UPDATE RESTRICT + ON DELETE RESTRICT ) }, q{ -- Быстрый выбор всех чанков конкретного снимка (частый запрос). - CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot ON snapshot_chunks(snapshot_id) + CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot + ON snapshot_chunks(snapshot_id) }, q{ -- Быстрый обратный поиск: где используется данный blob (для GC/аналитики). - CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_sha ON snapshot_chunks(sha256) + -- Индекс по BLOB(32) хорошо работает для точного поиска sha256. + CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_sha + ON snapshot_chunks(sha256) + }, + // -- ------------------------------------------------------------ + // -- Триггеры для управления refcount и GC blob'ов + // -- ------------------------------------------------------------ + q{ + -- Инкремент счётчика при привязке чанка к снимку. + -- Обновляем last_seen_utc, чтобы фиксировать "живость" контента. + -- ВАЖНО: FK гарантирует, что соответствующий blobs.sha256 уже существует. + CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_ai + AFTER INSERT ON snapshot_chunks + BEGIN + UPDATE blobs + SET refcount = refcount + 1, + last_seen_utc = CURRENT_TIMESTAMP + WHERE sha256 = NEW.sha256; + END + }, + q{ + -- Декремент счётчика при удалении строки состава. + -- Если счётчик стал 0 - удаляем неиспользуемый blob (авто-GC). + CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_ad + AFTER DELETE ON snapshot_chunks + BEGIN + UPDATE blobs + SET refcount = refcount - 1 + WHERE sha256 = OLD.sha256; + + DELETE FROM blobs + WHERE sha256 = OLD.sha256 + AND refcount <= 0; + END + }, + q{ + -- Корректировка счётчиков при смене sha256 в составе. + -- Триггер срабатывает только при реальном изменении значения. + -- Предполагается, что NEW.sha256 существует в blobs (иначе FK не даст обновить). + CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_au + AFTER UPDATE OF sha256 ON snapshot_chunks + FOR EACH ROW + WHEN NEW.sha256 <> OLD.sha256 + BEGIN + UPDATE blobs + SET refcount = refcount - 1 + WHERE sha256 = OLD.sha256; + + DELETE FROM blobs + WHERE sha256 = OLD.sha256 + AND refcount <= 0; + + UPDATE blobs + SET refcount = refcount + 1, + last_seen_utc = CURRENT_TIMESTAMP + WHERE sha256 = NEW.sha256; + END + }, + q{ + -- Автоматическая смена статуса снимка на "ready", + -- когда сумма размеров его чанков стала равна source_length. + -- Примечание: простая эвристика; если потом удалишь/поменяешь чанки, + -- триггер ниже вернёт статус обратно на "pending". + CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_ready + AFTER INSERT ON snapshot_chunks + BEGIN + UPDATE snapshots + SET status = "ready" + WHERE id = NEW.snapshot_id + AND (SELECT COALESCE(SUM(size),0) + FROM snapshot_chunks + WHERE snapshot_id = NEW.snapshot_id) + = (SELECT source_length FROM snapshots WHERE id = NEW.snapshot_id); + END + }, + q{ + -- При удалении любого чанка снимок снова помечается как "pending". + -- Это простой безопасный фоллбэк; следующая вставка приравняет суммы и вернёт "ready". + CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_pending + AFTER DELETE ON snapshot_chunks + BEGIN + UPDATE snapshots + SET status = "pending" + WHERE id = OLD.snapshot_id; + END } ]; diff --git a/test/app.d b/test/app.d index a3526d6..5383fda 100644 --- a/test/app.d +++ b/test/app.d @@ -7,5 +7,5 @@ import std.file : read; void main() { auto cas = new CAS("/tmp/base.db"); - cas.saveSnapshot(cast(ubyte[]) read("/tmp/text")); + cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); }