From 8a9142234ec8fb8e658976813248637f5b974366 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 12 Sep 2025 23:37:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=20=D0=9E=D0=9E=D0=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dub.json | 3 +- source/cdcdb/cdc/cas.d | 185 ----------------------------- source/cdcdb/cdc/package.d | 3 - source/cdcdb/{cdc => }/core.d | 2 +- source/cdcdb/db/package.d | 4 - source/cdcdb/db/scheme.md | 197 ------------------------------- source/cdcdb/db/types.d | 57 --------- source/cdcdb/{db => }/dblite.d | 206 +++++++++++++++++++++------------ source/cdcdb/{cdc => }/gear.d | 0 source/cdcdb/package.d | 3 +- source/cdcdb/{db => }/scheme.d | 0 source/cdcdb/snapshot.d | 68 +++++++++++ source/cdcdb/storage.d | 154 ++++++++++++++++++++++++ test/app.d | 16 ++- 14 files changed, 372 insertions(+), 526 deletions(-) delete mode 100644 source/cdcdb/cdc/cas.d delete mode 100644 source/cdcdb/cdc/package.d rename source/cdcdb/{cdc => }/core.d (98%) delete mode 100644 source/cdcdb/db/package.d delete mode 100644 source/cdcdb/db/scheme.md delete mode 100644 source/cdcdb/db/types.d rename source/cdcdb/{db => }/dblite.d (79%) rename source/cdcdb/{cdc => }/gear.d (100%) rename source/cdcdb/{db => }/scheme.d (100%) create mode 100644 source/cdcdb/snapshot.d create mode 100644 source/cdcdb/storage.d diff --git a/dub.json b/dub.json index 41c0d00..f81e96b 100644 --- a/dub.json +++ b/dub.json @@ -11,8 +11,7 @@ "zstd": "~>0.2.1" }, "stringImportPaths": [ - "source/cdcdb/db", - "source/cdcdb/cdc" + "source/cdcdb" ], "configurations": [ { diff --git a/source/cdcdb/cdc/cas.d b/source/cdcdb/cdc/cas.d deleted file mode 100644 index 84cae33..0000000 --- a/source/cdcdb/cdc/cas.d +++ /dev/null @@ -1,185 +0,0 @@ -module cdcdb.cdc.cas; - -import cdcdb.db; -import cdcdb.cdc.core; - -import zstd; - -import std.digest.sha : SHA256, digest; -import std.format : format; -import std.exception : enforce; - -// Content-Addressable Storage (Контентно-адресуемая система хранения) -// CAS-хранилище со снапшотами -final class CAS -{ -private: - DBLite _db; - bool _zstd; - - size_t _minSize; - size_t _normalSize; - size_t _maxSize; - size_t _maskS; - size_t _maskL; - CDC _cdc; -public: - this( - string database, - bool zstd = false, - size_t busyTimeout = 3000, - size_t maxRetries = 3, - size_t minSize = 256, - size_t normalSize = 512, - size_t maxSize = 1024, - size_t maskS = 0xFF, - size_t maskL = 0x0F - ) { - _db = new DBLite(database, busyTimeout, maxRetries); - _zstd = zstd; - - _minSize = minSize; - _normalSize = normalSize; - _maxSize = maxSize; - _maskS = maskS; - _maskL = maskL; - - _cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL); - } - - size_t newSnapshot(string label, const(ubyte)[] data, string description = string.init) - { - if (data.length == 0) { - throw new Exception("Данные имеют нулевой размер"); - } - - ubyte[32] sha256 = digest!SHA256(data); - - // Если последний снимок файла соответствует текущему состоянию - if (_db.isLast(label, sha256)) return 0; - - Snapshot snapshot; - - snapshot.label = label; - snapshot.sha256 = sha256; - snapshot.description = description; - snapshot.sourceLength = data.length; - snapshot.algoMin = _minSize; - snapshot.algoNormal = _normalSize; - snapshot.algoMax = _maxSize; - snapshot.maskS = _maskS; - snapshot.maskL = _maskL; - - _db.beginImmediate(); - - bool ok; - - scope (exit) - { - if (!ok) - _db.rollback(); - } - scope (success) - { - _db.commit(); - } - - auto idSnapshot = _db.addSnapshot(snapshot); - - SnapshotChunk snapshotChunk; - Blob blob; - - blob.zstd = _zstd; - - // Разбить на фрагменты - Chunk[] chunks = _cdc.split(data); - - // Запись фрагментов в БД - foreach (chunk; chunks) - { - blob.sha256 = chunk.sha256; - blob.size = chunk.size; - - auto content = data[chunk.offset .. chunk.offset + chunk.size]; - - if (_zstd) { - ubyte[] zBytes = compress(content, 22); - size_t zSize = zBytes.length; - ubyte[32] zHash = digest!SHA256(zBytes); - - blob.zSize = zSize; - blob.zSha256 = zHash; - blob.content = zBytes; - } else { - blob.content = content.dup; - } - - // Запись фрагментов - _db.addBlob(blob); - - snapshotChunk.snapshotId = idSnapshot; - snapshotChunk.chunkIndex = chunk.index; - snapshotChunk.offset = chunk.offset; - snapshotChunk.sha256 = chunk.sha256; - - // Привязка фрагментов к снимку - _db.addSnapshotChunk(snapshotChunk); - } - - ok = true; - - return idSnapshot; - } - - Snapshot[] getSnapshots(string label = string.init) - { - return _db.getSnapshots(label); - } - - ubyte[] getSnapshotData(const ref Snapshot snapshot) - { - auto dataChunks = _db.getChunks(snapshot.id); - ubyte[] content; - - foreach (chunk; dataChunks) { - ubyte[] bytes; - if (chunk.zstd) { - enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому"); - bytes = cast(ubyte[]) uncompress(chunk.content); - } else { - bytes = chunk.content; - } - enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому"); - content ~= bytes; - } - enforce(snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает"); - - return content; - } - - void removeSnapshot(const ref Snapshot snapshot) - { - _db.beginImmediate(); - - bool ok; - - scope (exit) - { - if (!ok) - _db.rollback(); - } - scope (success) - { - _db.commit(); - } - - _db.deleteSnapshot(snapshot.id); - - ok = true; - } - - string getVersion() const @safe nothrow { - import cdcdb.version_; - return cdcdbVersion; - } -} diff --git a/source/cdcdb/cdc/package.d b/source/cdcdb/cdc/package.d deleted file mode 100644 index 428164e..0000000 --- a/source/cdcdb/cdc/package.d +++ /dev/null @@ -1,3 +0,0 @@ -module cdcdb.cdc; - -public import cdcdb.cdc.cas; diff --git a/source/cdcdb/cdc/core.d b/source/cdcdb/core.d similarity index 98% rename from source/cdcdb/cdc/core.d rename to source/cdcdb/core.d index 84ee7ce..874c23b 100644 --- a/source/cdcdb/cdc/core.d +++ b/source/cdcdb/core.d @@ -1,4 +1,4 @@ -module cdcdb.cdc.core; +module cdcdb.core; import std.digest.sha : SHA256, digest; diff --git a/source/cdcdb/db/package.d b/source/cdcdb/db/package.d deleted file mode 100644 index 5e2232c..0000000 --- a/source/cdcdb/db/package.d +++ /dev/null @@ -1,4 +0,0 @@ -module cdcdb.db; - -public import cdcdb.db.dblite; -public import cdcdb.db.types; diff --git a/source/cdcdb/db/scheme.md b/source/cdcdb/db/scheme.md deleted file mode 100644 index 70d2776..0000000 --- a/source/cdcdb/db/scheme.md +++ /dev/null @@ -1,197 +0,0 @@ -# Схемы базы данных для хранения снимков (фрагментов) - -## Структура базы данных -```mermaid -erDiagram - %% Композитный PK у SNAPSHOT_CHUNKS: (snapshot_id, chunk_index) - - SNAPSHOTS { - int id PK - string label - string created_utc - int source_length - int algo_min - int algo_normal - int algo_max - int mask_s - int mask_l - string status - } - - BLOBS { - string sha256 PK - int size - blob content - string created_utc - } - - SNAPSHOT_CHUNKS { - int snapshot_id FK - int chunk_index - int offset - int size - string sha256 FK - } - - %% Связи и поведение внешних ключей - SNAPSHOTS ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE CASCADE" - BLOBS ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE RESTRICT" -``` - -## Схема последовательности записи в базу данных - -```mermaid -sequenceDiagram - autonumber - participant APP as Приложение - participant CH as Разбиение на чанки (FastCDC) - participant HS as Хеширование (SHA-256) - participant DB as База данных (SQLite) - - Note over APP,DB: Подготовка - APP->>DB: Открывает соединение, включает PRAGMA (WAL, foreign_keys=ON) - APP->>DB: BEGIN IMMEDIATE (начать транзакцию с блокировкой на запись) - - Note over APP,DB: Создание метаданных снимка - APP->>DB: INSERT INTO snapshots(label, source_length, algo_min, algo_normal, algo_max, mask_s, mask_l, status='pending') - DB-->>APP: id снимка = last_insert_rowid() - - Note over APP,CH: Поток файла → чанки - APP->>CH: Читает файл, передает параметры FastCDC (min/normal/max, mask_s/mask_l) - loop Для каждого чанка в порядке следования - CH-->>APP: Возвращает {chunk_index, offset, size, bytes} - - Note over APP,HS: Хеш содержимого - APP->>HS: Вычисляет SHA-256(bytes) - HS-->>APP: digest (sha256) - - Note over APP,DB: Дедупликация контента - APP->>DB: SELECT 1 FROM blobs WHERE sha256 = ? - alt Блоб отсутствует - APP->>DB: INSERT INTO blobs(sha256, size, content) - DB-->>APP: OK - else Блоб уже есть - DB-->>APP: Найден (пропускаем вставку содержимого) - end - - Note over APP,DB: Привязка чанка к снимку - APP->>DB: INSERT INTO snapshot_chunks(snapshot_id, chunk_index, offset, size, sha256) - DB-->>APP: OK (PK: (snapshot_id, chunk_index)) - end - - Note over APP,DB: Валидация и завершение - APP->>DB: SELECT SUM(size) FROM snapshot_chunks WHERE snapshot_id = ? - DB-->>APP: total_size - alt total_size == snapshots.source_length - APP->>DB: UPDATE snapshots SET status='ready' WHERE id = ? - APP->>DB: COMMIT - DB-->>APP: Транзакция зафиксирована - else Несоответствие размеров или ошибка - APP->>DB: ROLLBACK - DB-->>APP: Откат изменений - APP-->>APP: Логирует ошибку, возвращает код/исключение - end -``` - -## Схема последовательности восстановления из базы данных - -```mermaid -sequenceDiagram - autonumber - participant APP as Приложение - participant DB as База данных (SQLite) - participant FS as Целевой файл - participant HS as Хеширование (опц.) - - Note over APP,DB: Подготовка к чтению - APP->>DB: Открывает соединение (read), BEGIN (снимок чтения) - - Note over APP,DB: Выбор снимка - APP->>DB: Находит нужный снимок по id/label, читает status и source_length - DB-->>APP: id, status, source_length - alt status == "ready" - else снимок не готов - APP-->>APP: Прерывает восстановление с ошибкой - DB-->>APP: END - end - - Note over APP,DB: Получение состава снимка - APP->>DB: SELECT chunk_index, offset, size, sha256 FROM snapshot_chunks WHERE snapshot_id=? ORDER BY chunk_index - DB-->>APP: Строки чанков в порядке chunk_index - - loop Для каждого чанка - APP->>DB: SELECT content, size FROM blobs WHERE sha256=? - DB-->>APP: content, blob_size - - Note over APP,HS: (опц.) контроль целостности чанка - APP->>HS: Вычисляет SHA-256(content) - HS-->>APP: digest - APP-->>APP: Сверяет digest с sha256 и size с blob_size - - alt offset задан - APP->>FS: Позиционируется на offset и пишет content (pwrite/seek+write) - else offset отсутствует - APP->>FS: Дописывает content в конец файла - end - end - - Note over APP,DB: Финальная проверка - APP-->>APP: Суммирует размеры записанных чанков → total_size - APP->>DB: Берёт snapshots.source_length - DB-->>APP: source_length - alt total_size == source_length - APP->>FS: fsync и close - DB-->>APP: END - APP-->>APP: Успешное восстановление - else размеры не совпали - APP->>FS: Удаляет/помечает файл как повреждённый - DB-->>APP: END - APP-->>APP: Фиксирует ошибку (несоответствие сумм) - end -``` - -## Схема записи в БД - -```mermaid -sequenceDiagram - autonumber - participant APP as Приложение - participant DB as SQLite - participant CH as Разбиение (FastCDC) - participant HS as SHA-256 - - Note over APP,DB: Подготовка к записи - APP->>DB: PRAGMA foreign_keys=ON - APP->>DB: BEGIN IMMEDIATE - - Note over APP,DB: Метаданные снимка - APP->>DB: INSERT INTO snapshots(..., status='pending') - DB-->>APP: snap_id := last_insert_rowid() - - Note over APP,CH: Поток файла → чанки (min/normal/max, mask_s/mask_l) - loop Для каждого чанка по порядку - CH-->>APP: {chunk_index, offset, size, bytes} - - Note over APP,HS: Хеширование - APP->>HS: SHA-256(bytes) - HS-->>APP: sha256 (32 байта) - - Note over APP,DB: Дедупликация содержимого - APP->>DB: INSERT INTO blobs(sha256,size,content) ON CONFLICT DO NOTHING - DB-->>APP: OK (новая строка или уже была) - - Note over APP,DB: Привязка к снимку - APP->>DB: INSERT INTO snapshot_chunks(snapshot_id,chunk_index,offset,size,sha256) - DB-->>APP: OK (триггер ++refcount, last_seen_utc=now) - end - - Note over APP,DB: Валидация и финал - APP->>DB: SELECT SUM(size) FROM snapshot_chunks WHERE snapshot_id = snap_id - DB-->>APP: total_size - alt total_size == snapshots.source_length - Note over DB: триггер mark_ready ставит status='ready' - APP->>DB: COMMIT - else несовпадение / ошибка - APP->>DB: ROLLBACK - end -``` diff --git a/source/cdcdb/db/types.d b/source/cdcdb/db/types.d deleted file mode 100644 index f1c4e53..0000000 --- a/source/cdcdb/db/types.d +++ /dev/null @@ -1,57 +0,0 @@ -module cdcdb.db.types; - -import std.datetime : DateTime; - -enum SnapshotStatus : int -{ - pending = 0, - ready = 1 -} - -struct Snapshot -{ - long id; - string label; - ubyte[32] sha256; - string description; - DateTime createdUtc; - long sourceLength; - long algoMin; - long algoNormal; - long algoMax; - long maskS; - long maskL; - SnapshotStatus status; -} - -struct Blob -{ - ubyte[32] sha256; - ubyte[32] zSha256; - long size; - long zSize; - ubyte[] content; - DateTime createdUtc; - DateTime lastSeenUtc; - long refcount; - bool zstd; -} - -struct SnapshotChunk -{ - long snapshotId; - long chunkIndex; - long offset; - ubyte[32] sha256; -} - -struct SnapshotDataChunk { - long chunkIndex; - long offset; - long size; - ubyte[] content; - bool zstd; - long zSize; - ubyte[32] sha256; - ubyte[32] zSha256; -} diff --git a/source/cdcdb/db/dblite.d b/source/cdcdb/dblite.d similarity index 79% rename from source/cdcdb/db/dblite.d rename to source/cdcdb/dblite.d index 361d763..f6d0beb 100644 --- a/source/cdcdb/db/dblite.d +++ b/source/cdcdb/dblite.d @@ -1,14 +1,66 @@ -module cdcdb.db.dblite; +module cdcdb.dblite; -import cdcdb.db.types; +import arsd.sqlite : Sqlite, SqliteResult, DatabaseException; -import arsd.sqlite; - -import std.exception : enforce; -import std.conv : to; +import std.datetime : DateTime; import std.string : join, replace, toLower; import std.algorithm : canFind; +import std.conv : to; import std.format : format; +import std.exception : enforce; + +enum SnapshotStatus : ubyte +{ + pending = 0, + ready = 1 +} + +struct DBSnapshot { + long id; + string label; + ubyte[32] sha256; + string description; + DateTime createdUtc; + long sourceLength; + long algoMin; + long algoNormal; + long algoMax; + long maskS; + long maskL; + SnapshotStatus status; +} + +struct DBSnapshotChunk +{ + long snapshotId; + long chunkIndex; + long offset; + ubyte[32] sha256; +} + +struct DBBlob +{ + ubyte[32] sha256; + ubyte[32] zSha256; + long size; + long zSize; + ubyte[] content; + DateTime createdUtc; + DateTime lastSeenUtc; + long refcount; + bool zstd; +} + +struct DBSnapshotChunkData { + long chunkIndex; + long offset; + long size; + ubyte[] content; + bool zstd; + long zSize; + ubyte[32] sha256; + ubyte[32] zSha256; +} final class DBLite : Sqlite { @@ -120,7 +172,25 @@ public: sql("ROLLBACK"); } - long addSnapshot(Snapshot snapshot) + bool isLast(string label, ubyte[] sha256) { + auto queryResult = sql( + q{ + SELECT COALESCE( + (SELECT (label = ? AND sha256 = ?) + FROM snapshots + ORDER BY created_utc DESC + LIMIT 1), + 0 + ) AS is_last; + }, label, sha256 + ); + + if (!queryResult.empty()) + return queryResult.front()["is_last"].to!long > 0; + return false; + } + + long addSnapshot(DBSnapshot snapshot) { auto queryResult = sql( q{ @@ -157,13 +227,14 @@ public: return queryResult.front()["id"].to!long; } - void addBlob(Blob blob) + bool addBlob(DBBlob blob) { - sql( + auto queryResult = sql( q{ INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd) VALUES (?,?,?,?,?,?) ON CONFLICT (sha256) DO NOTHING + RETURNING sha256 }, blob.sha256[], blob.zstd ? blob.zSha256[] : null, @@ -172,76 +243,28 @@ public: blob.content, blob.zstd.to!int ); + + return !queryResult.empty(); } - void addSnapshotChunk(SnapshotChunk snapshotChunk) + bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) { - sql( + auto queryResult = sql( q{ INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256) VALUES(?,?,?,?) + RETURNING snapshot_id }, snapshotChunk.snapshotId, snapshotChunk.chunkIndex, snapshotChunk.offset, snapshotChunk.sha256[] ); + + return !queryResult.empty(); } - bool isLast(string label, ubyte[] sha256) { - auto queryResult = sql( - q{ - SELECT COALESCE( - (SELECT (label = ? AND sha256 = ?) - FROM snapshots - ORDER BY created_utc DESC - LIMIT 1), - 0 - ) AS is_last; - }, label, sha256 - ); - - if (!queryResult.empty()) - return queryResult.front()["is_last"].to!long > 0; - return false; - } - - Snapshot[] getSnapshots(string label) - { - auto queryResult = sql( - q{ - SELECT id, label, sha256, description, created_utc, source_length, - algo_min, algo_normal, algo_max, mask_s, mask_l, status - FROM snapshots WHERE (length(?) = 0 OR label = ?1); - }, label - ); - - Snapshot[] snapshots; - - foreach (row; queryResult) - { - Snapshot snapshot; - - snapshot.id = row["id"].to!long; - snapshot.label = row["label"].to!string; - snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; - snapshot.description = row["description"].to!string; - snapshot.createdUtc = toDateTime(row["created_utc"].to!string); - snapshot.sourceLength = row["source_length"].to!long; - snapshot.algoMin = row["algo_min"].to!long; - snapshot.algoNormal = row["algo_normal"].to!long; - snapshot.algoMax = row["algo_max"].to!long; - snapshot.maskS = row["mask_s"].to!long; - snapshot.maskL = row["mask_l"].to!long; - snapshot.status = cast(SnapshotStatus) row["status"].to!int; - - snapshots ~= snapshot; - } - - return snapshots; - } - - Snapshot getSnapshot(long id) + DBSnapshot getSnapshot(long id) { auto queryResult = sql( q{ @@ -251,7 +274,7 @@ public: }, id ); - Snapshot snapshot; + DBSnapshot snapshot; if (!queryResult.empty()) { @@ -274,11 +297,42 @@ public: return snapshot; } - void deleteSnapshot(long id) { - sql("DELETE FROM snapshots WHERE id = ?", id); + DBSnapshot[] getSnapshots(string label) + { + auto queryResult = sql( + q{ + SELECT id, label, sha256, description, created_utc, source_length, + algo_min, algo_normal, algo_max, mask_s, mask_l, status + FROM snapshots WHERE (length(?) = 0 OR label = ?1); + }, label + ); + + DBSnapshot[] snapshots; + + foreach (row; queryResult) + { + DBSnapshot snapshot; + + snapshot.id = row["id"].to!long; + snapshot.label = row["label"].to!string; + snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; + snapshot.description = row["description"].to!string; + snapshot.createdUtc = toDateTime(row["created_utc"].to!string); + snapshot.sourceLength = row["source_length"].to!long; + snapshot.algoMin = row["algo_min"].to!long; + snapshot.algoNormal = row["algo_normal"].to!long; + snapshot.algoMax = row["algo_max"].to!long; + snapshot.maskS = row["mask_s"].to!long; + snapshot.maskL = row["mask_l"].to!long; + snapshot.status = cast(SnapshotStatus) row["status"].to!int; + + snapshots ~= snapshot; + } + + return snapshots; } - SnapshotDataChunk[] getChunks(long snapshotId) + DBSnapshotChunkData[] getChunks(long snapshotId) { auto queryResult = sql( q{ @@ -291,11 +345,11 @@ public: }, snapshotId ); - SnapshotDataChunk[] sdchs; + DBSnapshotChunkData[] sdchs; foreach (row; queryResult) { - SnapshotDataChunk sdch; + DBSnapshotChunkData sdch; sdch.chunkIndex = row["chunk_index"].to!long; sdch.offset = row["offset"].to!long; @@ -311,4 +365,14 @@ public: return sdchs; } + + long deleteSnapshot(long id) { + auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id); + + if (queryResult.empty()) { + throw new Exception("Ошибка при удалении снимка из базы данных"); + } + + return queryResult.front()["id"].to!long; + } } diff --git a/source/cdcdb/cdc/gear.d b/source/cdcdb/gear.d similarity index 100% rename from source/cdcdb/cdc/gear.d rename to source/cdcdb/gear.d diff --git a/source/cdcdb/package.d b/source/cdcdb/package.d index 50d04a9..167d305 100644 --- a/source/cdcdb/package.d +++ b/source/cdcdb/package.d @@ -1,3 +1,4 @@ module cdcdb; -public import cdcdb.cdc; +public import cdcdb.storage; +public import cdcdb.snapshot; diff --git a/source/cdcdb/db/scheme.d b/source/cdcdb/scheme.d similarity index 100% rename from source/cdcdb/db/scheme.d rename to source/cdcdb/scheme.d diff --git a/source/cdcdb/snapshot.d b/source/cdcdb/snapshot.d new file mode 100644 index 0000000..939f4ed --- /dev/null +++ b/source/cdcdb/snapshot.d @@ -0,0 +1,68 @@ +module cdcdb.snapshot; + +import cdcdb.dblite; + +import std.exception : enforce; + +final class Snapshot { +private: + DBLite _db; + DBSnapshot _snapshot; +public: + this(DBLite dblite, DBSnapshot dbSnapshot) { + _db = dblite; + _snapshot = dbSnapshot; + } + + this(DBLite dblite, long idSnapshot) { + _db = dblite; + _snapshot = _db.getSnapshot(idSnapshot); + } + + ubyte[] data() { + auto dataChunks = _db.getChunks(_snapshot.id); + ubyte[] content; + + import zstd : uncompress; + + foreach (chunk; dataChunks) { + ubyte[] bytes; + if (chunk.zstd) { + enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому"); + bytes = cast(ubyte[]) uncompress(chunk.content); + } else { + bytes = chunk.content; + } + enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому"); + content ~= bytes; + } + + import std.digest.sha : SHA256, digest; + + enforce(_snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает"); + + return content; + } + + bool remove() { + _db.beginImmediate(); + + bool ok; + + scope (exit) + { + if (!ok) + _db.rollback(); + } + scope (success) + { + _db.commit(); + } + + long idDeleted = _db.deleteSnapshot(_snapshot.id); + + ok = true; + + return _snapshot.id == idDeleted; + } +} diff --git a/source/cdcdb/storage.d b/source/cdcdb/storage.d new file mode 100644 index 0000000..4438e72 --- /dev/null +++ b/source/cdcdb/storage.d @@ -0,0 +1,154 @@ +module cdcdb.storage; + +import cdcdb.dblite; +import cdcdb.core; +import cdcdb.snapshot; + +final class Storage +{ +private: + // Параметры работы с базой данных + DBLite _db; + bool _zstd; + // Настройки CDC механизма + CDC _cdc; + size_t _minSize; + size_t _normalSize; + size_t _maxSize; + size_t _maskS; + size_t _maskL; + + void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024, + size_t maskS = 0xFF, size_t maskL = 0x0F) + { + _minSize = minSize; + _normalSize = normalSize; + _maxSize = maxSize; + _maskS = maskS; + _maskL = maskL; + // CDC не хранит динамически выделенных данных, переинициализация безопасна + _cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL); + } + +public: + this(string database, bool zstd = false, size_t busyTimeout = 3000, size_t maxRetries = 3) + { + _db = new DBLite(database, busyTimeout, maxRetries); + _zstd = zstd; + initCDC(); + } + + void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, size_t maskS, size_t maskL) + { + initCDC(minSize, normalSize, maxSize, maskS, maskL); + } + + Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init) + { + if (data.length == 0) + { + throw new Exception("Данные имеют нулевой размер"); + } + + import std.digest.sha : SHA256, digest; + + ubyte[32] sha256 = digest!SHA256(data); + + // Если последний снимок файла соответствует текущему состоянию + if (_db.isLast(label, sha256)) + return null; + + DBSnapshot dbSnapshot; + + dbSnapshot.label = label; + dbSnapshot.sha256 = sha256; + dbSnapshot.description = description; + dbSnapshot.sourceLength = data.length; + dbSnapshot.algoMin = _minSize; + dbSnapshot.algoNormal = _normalSize; + dbSnapshot.algoMax = _maxSize; + dbSnapshot.maskS = _maskS; + dbSnapshot.maskL = _maskL; + + _db.beginImmediate(); + + bool ok; + + scope (exit) + { + if (!ok) + _db.rollback(); + } + scope (success) + { + _db.commit(); + } + + auto idSnapshot = _db.addSnapshot(dbSnapshot); + + DBSnapshotChunk dbSnapshotChunk; + DBBlob dbBlob; + + dbBlob.zstd = _zstd; + + // Разбить на фрагменты + Chunk[] chunks = _cdc.split(data); + + import zstd : compress; + + // Запись фрагментов в БД + foreach (chunk; chunks) + { + dbBlob.sha256 = chunk.sha256; + dbBlob.size = chunk.size; + + auto content = data[chunk.offset .. chunk.offset + chunk.size]; + + if (_zstd) { + ubyte[] zBytes = compress(content, 22); + size_t zSize = zBytes.length; + ubyte[32] zHash = digest!SHA256(zBytes); + + dbBlob.zSize = zSize; + dbBlob.zSha256 = zHash; + dbBlob.content = zBytes; + } else { + dbBlob.content = content.dup; + } + + // Запись фрагментов + _db.addBlob(dbBlob); + + dbSnapshotChunk.snapshotId = idSnapshot; + dbSnapshotChunk.chunkIndex = chunk.index; + dbSnapshotChunk.offset = chunk.offset; + dbSnapshotChunk.sha256 = chunk.sha256; + + // Привязка фрагментов к снимку + _db.addSnapshotChunk(dbSnapshotChunk); + } + + ok = true; + + Snapshot snapshot = new Snapshot(_db, idSnapshot); + + return snapshot; + } + + Snapshot[] getSnapshots(string label = string.init) { + Snapshot[] snapshots; + + foreach (snapshot; _db.getSnapshots(label)) { + snapshots ~= new Snapshot(_db, snapshot); + } + + return snapshots; + } + + string getVersion() const @safe nothrow + { + import cdcdb.version_ : cdcdbVersion; + + return cdcdbVersion; + } +} diff --git a/test/app.d b/test/app.d index 919d8c8..fa658d8 100644 --- a/test/app.d +++ b/test/app.d @@ -6,12 +6,18 @@ import std.file : read; void main() { - auto cas = new CAS("/tmp/base.db", true); - cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); - // import std.stdio : writeln; + auto storage = new Storage("/tmp/base.db", true); + storage.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); - foreach (snapshot; cas.getSnapshots()) { - writeln(snapshot); + // if (snapshot !is null) { + // writeln(cast(string) snapshot.data); + // snapshot.remove(); + // } + + import std.stdio : writeln; + + foreach (snapshot; storage.getSnapshots()) { + writeln(cast(string) snapshot.data); } // writeln(cas.getVersion);