diff --git a/dub.json b/dub.json index f81e96b..41c0d00 100644 --- a/dub.json +++ b/dub.json @@ -11,7 +11,8 @@ "zstd": "~>0.2.1" }, "stringImportPaths": [ - "source/cdcdb" + "source/cdcdb/db", + "source/cdcdb/cdc" ], "configurations": [ { diff --git a/source/cdcdb/cdc/cas.d b/source/cdcdb/cdc/cas.d new file mode 100644 index 0000000..84cae33 --- /dev/null +++ b/source/cdcdb/cdc/cas.d @@ -0,0 +1,185 @@ +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/core.d b/source/cdcdb/cdc/core.d similarity index 98% rename from source/cdcdb/core.d rename to source/cdcdb/cdc/core.d index 874c23b..84ee7ce 100644 --- a/source/cdcdb/core.d +++ b/source/cdcdb/cdc/core.d @@ -1,4 +1,4 @@ -module cdcdb.core; +module cdcdb.cdc.core; import std.digest.sha : SHA256, digest; diff --git a/source/cdcdb/gear.d b/source/cdcdb/cdc/gear.d similarity index 100% rename from source/cdcdb/gear.d rename to source/cdcdb/cdc/gear.d diff --git a/source/cdcdb/cdc/package.d b/source/cdcdb/cdc/package.d new file mode 100644 index 0000000..428164e --- /dev/null +++ b/source/cdcdb/cdc/package.d @@ -0,0 +1,3 @@ +module cdcdb.cdc; + +public import cdcdb.cdc.cas; diff --git a/source/cdcdb/dblite.d b/source/cdcdb/db/dblite.d similarity index 79% rename from source/cdcdb/dblite.d rename to source/cdcdb/db/dblite.d index f6d0beb..361d763 100644 --- a/source/cdcdb/dblite.d +++ b/source/cdcdb/db/dblite.d @@ -1,66 +1,14 @@ -module cdcdb.dblite; +module cdcdb.db.dblite; -import arsd.sqlite : Sqlite, SqliteResult, DatabaseException; +import cdcdb.db.types; -import std.datetime : DateTime; +import arsd.sqlite; + +import std.exception : enforce; +import std.conv : to; 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 { @@ -172,25 +120,7 @@ public: sql("ROLLBACK"); } - 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) + long addSnapshot(Snapshot snapshot) { auto queryResult = sql( q{ @@ -227,14 +157,13 @@ public: return queryResult.front()["id"].to!long; } - bool addBlob(DBBlob blob) + void addBlob(Blob blob) { - auto queryResult = sql( + 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, @@ -243,28 +172,76 @@ public: blob.content, blob.zstd.to!int ); - - return !queryResult.empty(); } - bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) + void addSnapshotChunk(SnapshotChunk snapshotChunk) { - auto queryResult = sql( + 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(); } - DBSnapshot getSnapshot(long id) + 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) { auto queryResult = sql( q{ @@ -274,7 +251,7 @@ public: }, id ); - DBSnapshot snapshot; + Snapshot snapshot; if (!queryResult.empty()) { @@ -297,42 +274,11 @@ public: return snapshot; } - 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; + void deleteSnapshot(long id) { + sql("DELETE FROM snapshots WHERE id = ?", id); } - DBSnapshotChunkData[] getChunks(long snapshotId) + SnapshotDataChunk[] getChunks(long snapshotId) { auto queryResult = sql( q{ @@ -345,11 +291,11 @@ public: }, snapshotId ); - DBSnapshotChunkData[] sdchs; + SnapshotDataChunk[] sdchs; foreach (row; queryResult) { - DBSnapshotChunkData sdch; + SnapshotDataChunk sdch; sdch.chunkIndex = row["chunk_index"].to!long; sdch.offset = row["offset"].to!long; @@ -365,14 +311,4 @@ 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/db/package.d b/source/cdcdb/db/package.d new file mode 100644 index 0000000..5e2232c --- /dev/null +++ b/source/cdcdb/db/package.d @@ -0,0 +1,4 @@ +module cdcdb.db; + +public import cdcdb.db.dblite; +public import cdcdb.db.types; diff --git a/source/cdcdb/scheme.d b/source/cdcdb/db/scheme.d similarity index 100% rename from source/cdcdb/scheme.d rename to source/cdcdb/db/scheme.d diff --git a/source/cdcdb/db/scheme.md b/source/cdcdb/db/scheme.md new file mode 100644 index 0000000..70d2776 --- /dev/null +++ b/source/cdcdb/db/scheme.md @@ -0,0 +1,197 @@ +# Схемы базы данных для хранения снимков (фрагментов) + +## Структура базы данных +```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 new file mode 100644 index 0000000..f1c4e53 --- /dev/null +++ b/source/cdcdb/db/types.d @@ -0,0 +1,57 @@ +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/package.d b/source/cdcdb/package.d index 167d305..50d04a9 100644 --- a/source/cdcdb/package.d +++ b/source/cdcdb/package.d @@ -1,4 +1,3 @@ module cdcdb; -public import cdcdb.storage; -public import cdcdb.snapshot; +public import cdcdb.cdc; diff --git a/source/cdcdb/snapshot.d b/source/cdcdb/snapshot.d deleted file mode 100644 index 28d0e5d..0000000 --- a/source/cdcdb/snapshot.d +++ /dev/null @@ -1,142 +0,0 @@ -module cdcdb.snapshot; - -import cdcdb.dblite; - -import zstd : uncompress; - -import std.digest.sha : SHA256, digest, SHA256Digest; -import std.datetime : DateTime; -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 chunks = _db.getChunks(_snapshot.id); - ubyte[] content; - - foreach (chunk; chunks) - { - 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, "Оригинальный размер не соответствует ожидаемому"); - enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш-сумма фрагмента не совпадает"); - content ~= bytes; - } - - enforce(_snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает"); - - return content; - } - - void data(void delegate(const(ubyte)[]) sink) - { - auto chunks = _db.getChunks(_snapshot.id); - auto fctx = new SHA256Digest(); - - foreach (chunk; chunks) - { - 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, "Оригинальный размер не соответствует ожидаемому"); - enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш-сумма фрагмента не совпадает"); - - sink(bytes); - fctx.put(bytes); - } - - enforce(_snapshot.sha256 = fctx.finish(), "Хеш-сумма файла не совпадает"); - } - - 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; - } - - @property long id() const nothrow @safe - { - return _snapshot.id; - } - - @property string label() const @safe - { - return _snapshot.label; - } - - @property DateTime created() const @safe - { - return _snapshot.createdUtc; - } - - @property long length() const nothrow @safe - { - return _snapshot.sourceLength; - } - - @property ubyte[32] sha256() const nothrow @safe - { - return _snapshot.sha256; - } - - @property string status() const - { - import std.conv : to; - return _snapshot.status.to!string; - } - - @property string description() const nothrow @safe - { - return _snapshot.description; - } -} diff --git a/source/cdcdb/storage.d b/source/cdcdb/storage.d deleted file mode 100644 index 2b1da7c..0000000 --- a/source/cdcdb/storage.d +++ /dev/null @@ -1,156 +0,0 @@ -module cdcdb.storage; - -import cdcdb.dblite; -import cdcdb.core; -import cdcdb.snapshot; - -import zstd : compress, Level; - -final class Storage -{ -private: - // Параметры работы с базой данных - DBLite _db; - bool _zstd; - int _level; - // Настройки 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, int level = Level.base, size_t busyTimeout = 3000, size_t maxRetries = 3) - { - _db = new DBLite(database, busyTimeout, maxRetries); - _zstd = zstd; - _level = level; - 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); - - // Запись фрагментов в БД - 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, _level); - 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 0fe107e..919d8c8 100644 --- a/test/app.d +++ b/test/app.d @@ -2,26 +2,17 @@ import std.stdio; import cdcdb; -import std.file : read, write; -import std.stdio : File, writeln; -import std.conv : to; - +import std.file : read; void main() { - auto storage = new Storage("/tmp/base.db", true, 22); - storage.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); + auto cas = new CAS("/tmp/base.db", true); + cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); + // import std.stdio : writeln; - // if (snapshot !is null) { - // writeln(cast(string) snapshot.data); - // snapshot.remove(); - // } - - foreach (snapshot; storage.getSnapshots()) { - auto file = File("/tmp/restore" ~ snapshot.id.to!string, "wb"); - snapshot.data((const(ubyte)[] content) { - file.rawWrite(content); - }); - file.close(); + foreach (snapshot; cas.getSnapshots()) { + writeln(snapshot); } + + // writeln(cas.getVersion); }