commit dc0c8349c77c190cb1fd18d2d9c984208cd28078 Author: Alexander Zhirov Date: Tue Sep 9 19:39:22 2025 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fdd0e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.dub +docs.json +__dummy.html +docs/ +/cdcdb +cdcdb.so +cdcdb.dylib +cdcdb.dll +cdcdb.a +cdcdb.lib +cdcdb-test-* +*.exe +*.pdb +*.o +*.obj +*.lst +bin +lib diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5615e67 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + // Используйте IntelliSense, чтобы узнать о возможных атрибутах. + // Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов. + // Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "code-d", + "request": "launch", + "dubBuild": true, + "name": "Build & Debug DUB project", + "cwd": "${command:dubWorkingDirectory}", + "program": "bin/${command:dubTarget}", + "args": [] + }, + { + "name": "Debug D Program with sudo-gdb", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/bin/dwatch", + "args": ["-d", "/tmp/scripts"], // Аргументы командной строки для программы, если нужны + "stopAtEntry": false, // Остановить на входе в main + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "miDebuggerPath": "/usr/bin/sudo-gdb", // Путь к вашему скрипту + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d1c022f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.detectIndentation": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8bee51 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# cdcdb + +Подход с использованием CDC (Capture Data Change) для хранения блоков данных в базе данных SQLite. + +## Сборка + +```bash +# Статическая библиотека +dub build -c static +# Динамическая библиотека +dub build -c dynamic +# Тест-утилита +dub build -c binary +``` diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..41c0d00 --- /dev/null +++ b/dub.json @@ -0,0 +1,38 @@ +{ + "authors": [ + "Alexander Zhirov" + ], + "copyright": "Copyright © 2025, Alexander Zhirov", + "description": "A CDC Approach for Storing Chunks in an SQLite Database.", + "license": "BSL-1.0", + "name": "cdcdb", + "dependencies": { + "arsd-official:sqlite": "~>12.0.0", + "zstd": "~>0.2.1" + }, + "stringImportPaths": [ + "source/cdcdb/db", + "source/cdcdb/cdc" + ], + "configurations": [ + { + "name": "static", + "targetType": "staticLibrary", + "targetPath": "lib", + "sourcePaths": ["source"] + }, + { + "name": "dynamic", + "targetType": "dynamicLibrary", + "targetPath": "lib", + "sourcePaths": ["source"] + }, + { + "name": "binary", + "targetType": "executable", + "targetPath": "bin", + "mainSourceFile": "test/app.d", + "sourcePaths": ["source", "test"] + } + ] +} diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..caa3e75 --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,7 @@ +{ + "fileVersion": 1, + "versions": { + "arsd-official": "12.0.0", + "zstd": "0.2.1" + } +} diff --git a/source/cdcdb/cdc/cas.d b/source/cdcdb/cdc/cas.d new file mode 100644 index 0000000..023c139 --- /dev/null +++ b/source/cdcdb/cdc/cas.d @@ -0,0 +1,36 @@ +module cdcdb.cdc.cas; + +import cdcdb.db; +import cdcdb.cdc.core; + +final class CAS +{ +private: + DBLite _db; +public: + this(string database) + { + _db = new DBLite(database); + } + + size_t saveSnapshot(const(ubyte)[] data) + { + // Параметры для CDC вынести в отдельные настройки (продумать) + auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F); + // Разбить на фрагменты + auto chunks = cdc.split(data); + + import std.stdio : writeln; + + _db.beginImmediate(); + // Записать фрагменты в БД + foreach (chunk; chunks) + { + writeln(chunk.index); + } + _db.commit(); + // Записать манифест в БД + // Вернуть ID манифеста + return 0; + } +} diff --git a/source/cdcdb/cdc/core.d b/source/cdcdb/cdc/core.d new file mode 100644 index 0000000..f09732c --- /dev/null +++ b/source/cdcdb/cdc/core.d @@ -0,0 +1,90 @@ +module cdcdb.cdc.core; + +import cdcdb.cdc.types; + +import std.digest.sha : SHA256, digest; + +final class CDC +{ +private: + size_t _minSize, _normalSize, _maxSize; + ulong _maskS, _maskL; + // _gear + mixin(import("gear.d")); + + size_t cut(const(ubyte)[] src) @safe nothrow + { + size_t size = src.length; + if (size == 0) + return 0; + if (size <= _minSize) + return size; + + if (size > _maxSize) + size = _maxSize; + auto normalSize = _normalSize; + if (size < normalSize) + normalSize = size; + + ulong fingerprint = 0; + size_t index; + + // инициализация без cut-check + while (index < _minSize) + { + fingerprint = (fingerprint << 1) + _gear[src[index]]; + ++index; + } + // строгая маска + while (index < normalSize) + { + fingerprint = (fingerprint << 1) + _gear[src[index]]; + if ((fingerprint & _maskS) == 0) + return index; + ++index; + } + // слабая маска + while (index < size) + { + fingerprint = (fingerprint << 1) + _gear[src[index]]; + if ((fingerprint & _maskL) == 0) + return index; + ++index; + } + return size; + } + +public: + this(size_t minSize, size_t normalSize, size_t maxSize, ulong maskS, ulong maskL) @safe @nogc nothrow + { + assert(minSize > 0 && minSize < normalSize && normalSize < maxSize, + "Неверные размеры: требуется min < normal < max и min > 0"); + _minSize = minSize; + _normalSize = normalSize; + _maxSize = maxSize; + _maskS = maskS; + _maskL = maskL; + } + + Chunk[] split(const(ubyte)[] data) @safe + { + Chunk[] chunks; + if (data.length == 0) + return chunks; + chunks.reserve(data.length / _normalSize); + size_t offset = 0; + size_t index = 1; + + while (offset < data.length) + { + auto size = cut(data[offset .. $]); + auto bytes = data[offset .. offset + size]; + ubyte[32] hash = digest!SHA256(bytes); + chunks ~= Chunk(index, offset, size, hash); + + offset += size; + ++index; + } + return chunks; + } +} diff --git a/source/cdcdb/cdc/gear.d b/source/cdcdb/cdc/gear.d new file mode 100644 index 0000000..2a96d77 --- /dev/null +++ b/source/cdcdb/cdc/gear.d @@ -0,0 +1,66 @@ +immutable ulong[256] _gear = [ + 0x2722039f43c57a70, 0x338c1bd5b7ac5204, 0xf9f2c73ff33c98c0, 0x7dee12e6cd31cb32, + 0x9688335e0f2decfd, 0x5307003c8e60b963, 0xfd2a2848eb358095, 0xc3614773074ee6b7, + 0x6e35235234b6ed0a, 0x9d4cfa9d8e3850cc, 0xaa1b3d8af8ad86bd, 0x79c6d2e28bfb333d, + 0x3df08966a00c33ec, 0xfd58bbf83f38c690, 0x5ef9ee9a4552545b, 0x099192a7e5599bdc, + 0xa8f2419947f21017, 0xd6a03d010f2fda7c, 0x1fe53de04074fc20, 0x75b5aff7c66605f8, + 0x1a94b7484bf509a9, 0xbf2e371a53466ded, 0xedcf13f8eb0f0fdf, 0xfba81285ead7dafe, + 0x839fb29274557fa5, 0xeefe64b15cc7f7f0, 0x7d15f8e862726515, 0x59b416e43cca2adc, + 0x9c2c925dcde12d4a, 0xf3df9373c3e63a07, 0x747cb5ec08ffa4ef, 0x26c93138f3f19b29, + 0xcdade11723bd59ed, 0xc7a6a7d0c18642cb, 0x88c2976f22f5d084, 0x7c48f54cdaf480fe, + 0x91ea7c7fd3d06d54, 0xed3e31236e8c9366, 0xa16da2234f420bc4, 0x5ee6136449d30974, + 0xe32a921921181e16, 0xa6ab2fb8c7212257, 0x754a8a581ce819ca, 0x22b2de3e7c7a2f57, + 0xd2773285e49b9160, 0x19b0449384554129, 0x145e7d2c46da460e, 0xdd720d0e79a3615d, + 0x621ae4f0ad576223, 0x4f6da235cc25d5c9, 0x6d6d39e005d67437, 0x5839c8f3d2f71122, + 0x691a668bc7f5c153, 0xb2eb484f8546c61d, 0x7955c346c34cbcdc, 0x413c2a0ba6fd0a88, + 0x29ad3d418323592b, 0xb7d04d0abf3f8d50, 0x76e742f91dfc77ea, 0x8a5c80d1a0d5ff5c, + 0xce1aa9b0bdd16adc, 0x74e4bd6f412c8186, 0xbf1dddc8f63dfc08, 0x11dcb84c1b5c32cb, + 0x3320ed259fc0d8c0, 0x13dbd4c934c58e01, 0x344b61dd3741a9f9, 0x935861bea84a6f81, + 0xaf70eea3844052f9, 0xc0a83f93799c2e81, 0xdd23b2a943a5af16, 0x05b4efd89b3e818b, + 0x75b2a3d0fe099aec, 0x5aab5599ae580c37, 0xe9b64238ed626a6b, 0xb63f47c31c31ec1d, + 0x0b50ee03c3425dde, 0xf287ebed924249f6, 0xe09eee62604318c4, 0x0d334cb1bd82bc13, + 0xc41abf3d95b18620, 0x869c3bc45e2c9edf, 0x526de53484e093c7, 0xc640fee4784fd9ce, + 0x761637787d81c0ea, 0x817bf175cb17e903, 0x94a4846f1158f988, 0x99c254e5f8e698e0, + 0xa4623d4d1b76352e, 0x326dae4493475c3a, 0xed2944be79511208, 0x163a0a9b65f40339, + 0x336489f8c2f6190c, 0x670d217f8e6bee33, 0x662e19c285c5a4a1, 0xcab8f4512d0b251a, + 0x61d4476812ed1017, 0x0ec77209307430af, 0x20a94905901093dc, 0xaa9fe2cae9ffa699, + 0xc75f757de6c045dc, 0x141ef38478656459, 0x9b3ce9c4e3dd7858, 0x3ab62b9aa45a3d0d, + 0x61a89423e18e5e68, 0x3802972ecadf592d, 0xcfc85c5724ff3af8, 0x381ee916e97a628a, + 0x2fa2c37a040e488a, 0x9813a505b4ca4036, 0xc4254f1aaf7b2f42, 0xe8a0720b79a1188d, + 0xe663a71adb5d53e3, 0x6e3b5927934102af, 0xbd8c502741b1fcb1, 0x1af6fa2fb1d2e5a6, + 0xc88d367a79d06f5d, 0x29fe7cdab66530d9, 0x34bef2ebe612d95f, 0x9ab6977a57db1fa2, + 0x73774fc29deac09a, 0x7832f37495fd28fb, 0x1559a3badfbd42a6, 0x7e6831522a50d2bc, + 0xddb8564f3aafe3b7, 0x86acb9eca71bc09d, 0x21b0a9469727d4fc, 0x26d3b66f525ebcab, + 0x77e3fd126fd97e3a, 0x5306b81a9fe2a92e, 0x7292138f116d8911, 0x285b466c9939b076, + 0x40527805d9a4379d, 0x8986c05119c7ca1e, 0x6a7890c402303c31, 0xb1b109dc109405bc, + 0x1d71f3997b288f30, 0xfa203ff4dc9ea72c, 0x8ae3eea975cc92da, 0x3468e4305eabb928, + 0xd79c37e720467df1, 0x011e6490c0f832d2, 0x29ce2ada8509647a, 0xb4e325b9f3ba783c, + 0xa812ca4fad720763, 0x0cdf098645ccb476, 0xf6b47e21637fcd76, 0x3597f48148a297de, + 0x5875212868ab81ec, 0x1ea36243940596bb, 0xfd93ac7c39a27586, 0xabb09b0f803a7214, + 0x8cc8ec1ea21a16af, 0x824a0db50ae906d1, 0x3d972fb701ca3e70, 0xda60d493e9a20bd0, + 0x97d282f6bda26087, 0x9bc8f7842af296d0, 0x14804a1663a0cf7e, 0x3b71cc25885e75f3, + 0x131adc05e336042b, 0x566aa36d26eee86c, 0x97d4c4d4fd4b0dd1, 0xd2407b1485c7bee1, + 0xcad613e7b92e6df1, 0xe3ceccd99d975088, 0x99e6b93ff96a2636, 0x1ad75dbed057f0d0, + 0x5e3ba609dd100c6e, 0x9c5efa00b33a18f3, 0xad89369e692bdb28, 0xf7a546fca26d1d7d, + 0x5813db1fe943575f, 0x24c3467f03a144ae, 0xc892f2ce492cb7c8, 0xc44672263508d34b, + 0xd400e1c0a5734a40, 0x3ca24ee74bf8e84f, 0xd83bd4e907c351a5, 0xe142297005fa9aa8, + 0x0f6d796cf68abda0, 0x6c8e25bc6d9ae2e8, 0xccc235f322a42cf3, 0xabaf39cea8ca450c, + 0x02b9cdf615a0d7b6, 0x8aaf7d8b55d4dc39, 0xbe2c2bc6ef13c6c5, 0x6ad98aa4a4bc610f, + 0x1051a62ac2a2b434, 0xbd167e6eba260d35, 0xb9b86ac04ac4f811, 0xabe8a6453e196739, + 0x439ff734b19246b4, 0xcea324040c9e8981, 0x87f55cf1035e1a22, 0xa227d679c33597f9, + 0xbf4d654b6cdd0015, 0xc0302ec55f87a46e, 0xed32173466c70a83, 0x8ceb757b648d2bf2, + 0x1873757a6d17446b, 0xeb0f366fea62e77e, 0x145aa2795d34dd93, 0x2fc378be4c471db0, + 0x6d1274fb8f6364a2, 0x602a56fd1cc36728, 0x5f8aa6e0c892b4b5, 0x33e2c5653d8b1ad6, + 0x1f6c8b2a004714f4, 0x4042b98d54acbfef, 0x4606386f11f6456f, 0xf56bd21a8a35c540, + 0xd2b23c57b3718e1f, 0x94726832fe96e61d, 0xa225b072752a823b, 0x0bd957cf585f8cda, + 0x533d819bb30b4221, 0xda0f9cff9a0115fa, 0xd14de3b8fe3354ea, 0xa96328e96d9364c0, + 0x9078dc0eff2676ab, 0x22585cd4521c6210, 0x5903254df4e402a5, 0x1b54b71b55ae697a, + 0xb899b86756b2aa39, 0x5d5d2dd5cd0bce8b, 0x7b3a78a4a0662015, 0xa9fbfc7678fc7931, + 0xa732d694f6ab64a0, 0x9fc960e7db3e9716, 0x76c765948f3c2ba5, 0x076a509dca2a4349, + 0xca5bfc5973661e59, 0x454ec4d49bddd45d, 0x56115e001997cee2, 0xd689eb8926051c7f, + 0xf50df8ca9c355e3f, 0x88a375a9f0492a69, 0xe059fd001d50439a, 0x765c5d6f66d5e788, + 0xaf57f4eea178f896, 0x06e8cca68730fbbd, 0xb7b1f6f86904ce4e, 0x3c3b10b0c08cf0bf, + 0x1e0e310524778bd4, 0xd65d7cd93cde7c69, 0x18543b187c77fcf3, 0x180f6cdd1af3a60a, + 0xe1cd4c2bc3656704, 0x218fdfc5aa282d00, 0x844eeaf2e439b242, 0x05df1a59e415b4c6, + 0x14abdd3ace097c2c, 0x7f3b0705b04b14d4, 0xf69c57f60180332b, 0x165fc3f0e65db80f +]; 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/cdc/types.d b/source/cdcdb/cdc/types.d new file mode 100644 index 0000000..c3a41f6 --- /dev/null +++ b/source/cdcdb/cdc/types.d @@ -0,0 +1,20 @@ +module cdcdb.cdc.types; + +/// Единица разбиения +struct Chunk +{ + size_t index; // 1..N + size_t offset; // смещение в исходном буфере + size_t size; // размер чанка + ubyte[32] sha256; // hex(SHA-256) содержимого +} + +/// Метаданные снимка +struct SnapshotInfo +{ + size_t id; + string createdUTC; // ISO-8601 + string label; + size_t sourceLength; + size_t chunks; +} diff --git a/source/cdcdb/db/dblite.d b/source/cdcdb/db/dblite.d new file mode 100644 index 0000000..184238c --- /dev/null +++ b/source/cdcdb/db/dblite.d @@ -0,0 +1,47 @@ +module cdcdb.db.dblite; + +import arsd.sqlite; +import std.file : exists, isFile; + +final class DBLite : Sqlite +{ +private: + string _dbPath; + // _scheme + mixin(import("scheme.d")); +public: + this(string database) + { + _dbPath = database; + super(database); + + foreach (schemeQuery; _scheme) + { + sql(schemeQuery); + } + + query("PRAGMA journal_mode=WAL"); + query("PRAGMA synchronous=NORMAL"); + query("PRAGMA foreign_keys=ON"); + } + + void beginImmediate() + { + query("BEGIN IMMEDIATE"); + } + + void commit() + { + query("COMMIT"); + } + + void rollback() + { + query("ROLLBACK"); + } + + SqliteResult sql(T...)(string queryText, T args) + { + return cast(SqliteResult) query(queryText, args); + } +} diff --git a/source/cdcdb/db/package.d b/source/cdcdb/db/package.d new file mode 100644 index 0000000..a15624b --- /dev/null +++ b/source/cdcdb/db/package.d @@ -0,0 +1,3 @@ +module cdcdb.db; + +public import cdcdb.db.dblite; diff --git a/source/cdcdb/db/scheme.d b/source/cdcdb/db/scheme.d new file mode 100644 index 0000000..557e0cf --- /dev/null +++ b/source/cdcdb/db/scheme.d @@ -0,0 +1,75 @@ +auto _scheme = [ + q{ + -- Метаданные снапшота + CREATE TABLE IF NOT EXISTS snapshots ( + -- Уникальный числовой идентификатор снимка. Используется во внешних ключах. + id INTEGER PRIMARY KEY AUTOINCREMENT, + -- Произвольная метка/название снимка. + label TEXT, + -- Время создания записи в UTC. По умолчанию - сейчас. + created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + -- Полная длина исходного файла в байтах для этого снимка (до разбиения на чанки). + source_length INTEGER NOT NULL, + -- Пороговые размеры FastCDC (минимальный/целевой/максимальный размер чанка) в байтах. + -- Фиксируются здесь, чтобы позже можно было корректно пересобрать/сравнить. + algo_min INTEGER NOT NULL, + algo_normal INTEGER NOT NULL, + algo_max INTEGER NOT NULL, + -- Маски для определения границ чанков (быстрый роллинг-хэш/FastCDC). + -- Обычно степени вида 2^n - 1. Хранятся для воспроизводимости. + mask_s INTEGER NOT NULL, + mask_l INTEGER NOT NULL, + -- Состояние снимка: + -- pending - метаданные созданы, состав не полностью загружен; + -- ready - все чанки привязаны, снимок готов к использованию. + status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready")) + ) + }, + q{ + -- Уникальные куски содержимого (сам контент в БД) + CREATE TABLE IF NOT EXISTS blobs ( + -- Хэш содержимого чанка. Ключ обеспечивает дедупликацию: одинаковый контент хранится один раз. + sha256 TEXT PRIMARY KEY, + -- Размер этого чанка в байтах. + size INTEGER NOT NULL, + -- Сырые байты чанка. + content BLOB NOT NULL, + -- Когда этот контент впервые появился в базе (UTC). + created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) + ) + }, + q{ + -- Состав снапшота (порядок чанков важен) + CREATE TABLE IF NOT EXISTS snapshot_chunks ( + -- Ссылка на snapshots.id. Определяет, к какому снимку относится строка. + snapshot_id INTEGER NOT NULL, + -- Позиция чанка в снимке (индексация). + -- Обеспечивает порядок сборки. + chunk_index INTEGER NOT NULL, + -- Смещение чанка в исходном файле в байтах. + -- Можно восстановить как сумму size предыдущих чанков по chunk_index, + -- но хранение ускоряет проверки/отладку. + offset INTEGER, + -- Размер именно этого чанка в байтах (дублирует blobs.size для быстрого доступа и валидации). + size INTEGER NOT NULL, + -- Ссылка на blobs.sha256. Привязывает позицию в снимке к конкретному содержимому. + sha256 TEXT 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 + ) + }, + q{ + -- Быстрый выбор всех чанков конкретного снимка (частый запрос). + 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) + } +]; diff --git a/source/cdcdb/db/scheme.md b/source/cdcdb/db/scheme.md new file mode 100644 index 0000000..5ba0d96 --- /dev/null +++ b/source/cdcdb/db/scheme.md @@ -0,0 +1,151 @@ +# Схемы базы данных для хранения снимков (фрагментов) + +## Структура базы данных +```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 +``` diff --git a/source/cdcdb/package.d b/source/cdcdb/package.d new file mode 100644 index 0000000..50d04a9 --- /dev/null +++ b/source/cdcdb/package.d @@ -0,0 +1,3 @@ +module cdcdb; + +public import cdcdb.cdc; diff --git a/test/app.d b/test/app.d new file mode 100644 index 0000000..a3526d6 --- /dev/null +++ b/test/app.d @@ -0,0 +1,11 @@ +import std.stdio; + +import cdcdb; + +import std.file : read; + +void main() +{ + auto cas = new CAS("/tmp/base.db"); + cas.saveSnapshot(cast(ubyte[]) read("/tmp/text")); +} diff --git a/tools/gen.d b/tools/gen.d new file mode 100755 index 0000000..1687d1d --- /dev/null +++ b/tools/gen.d @@ -0,0 +1,42 @@ +#!/usr/bin/rdmd + +import std.stdio : write, writef, writeln; +import std.random : Random, unpredictableSeed, uniform; + +void main() +{ + enum N = 256; + ulong[N] gear; + + auto rng = Random(unpredictableSeed); + + bool[ulong] seen; + ulong[] vals; + vals.reserve(N); + + while (vals.length < N) + { + const v = uniform!ulong(rng); + if (v in seen) + continue; + seen[v] = true; + vals ~= v; + } + + gear[] = vals[0 .. N]; + + writeln("immutable ulong[256] gear = ["); + foreach (i, v; gear) + { + if (i % 4 == 0) + write("\t"); + writef("0x%016x", v); + if (i != N - 1) + write(","); + if ((i + 1) % 4 == 0 || i == N - 1) + writeln(); + else + write(" "); + } + writeln("];"); +}