Compare commits

..

5 commits

18 changed files with 820 additions and 543 deletions

104
README.md
View file

@ -1,14 +1,102 @@
# cdcdb # cdcdb
Подход с использованием CDC (Capture Data Change) для хранения блоков данных в базе данных SQLite. A library for storing and managing snapshots of textual data in an SQLite database. It uses content-defined chunking (CDC) based on the FastCDC algorithm to split data into variable-size chunks for efficient deduplication. Supports optional Zstd compression, transactions, and end-to-end integrity verification via SHA-256. Primary use cases: backups and versioning of text files while minimizing storage footprint.
## Сборка ## FastCDC algorithm
FastCDC splits data into variable-size chunks using content hashing. A Gear table is used to compute rolling “fingerprints” and choose cut points while respecting minimum, target, and maximum chunk sizes. This efficiently detects changes and stores only unique chunks, reducing storage usage.
## Core classes
### Storage
High-level API for the SQLite store and snapshot management.
- **Constructor**: Initializes a connection to SQLite.
- **Methods**:
- `newSnapshot`: Creates a snapshot. Returns a `Snapshot` object or `null` if the data matches the latest snapshot.
- `getSnapshots`: Returns a list of snapshots (all, or filtered by label). Returns an array of `Snapshot`.
- `getSnapshot`: Fetches a snapshot by ID. Returns a `Snapshot`.
- `setupCDC`: Configures CDC splitting parameters. Returns nothing.
- `getVersion`: Returns the library version string (e.g., `"0.0.2"`).
- `removeSnapshots`: Deletes snapshots by label, ID, or a `Snapshot` object. Returns the number of deleted snapshots (for label) or `true`/`false` (for ID or object).
### Snapshot
Work with an individual snapshot.
- **Constructor**: Creates a snapshot handle by its ID.
- **Methods**:
- `data`: Restores full snapshot data. Returns a byte array (`ubyte[]`).
- `data`: Streams restored data via a delegate sink. Returns nothing.
- `remove`: Deletes the snapshot from the database. Returns `true` on success, otherwise `false`.
- **Properties**:
- `id`: Snapshot ID (`long`).
- `label`: Snapshot label (`string`).
- `created`: Creation timestamp (UTC, `DateTime`).
- `length`: Original data length (`long`).
- `sha256`: Data SHA-256 hash (`ubyte[32]`).
- `status`: Snapshot status (`"pending"` or `"ready"`).
- `description`: Snapshot description (`string`).
## Example
```d
import cdcdb;
import std.stdio : writeln, File;
import std.file : exists, remove;
void main()
{
// Create DB
string dbPath = "example.db";
// Initialize Storage with Zstd compression
auto storage = new Storage(dbPath, true, 22);
// Create a snapshot
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
auto snap = storage.newSnapshot("example_file", data, "Version 1.0");
if (snap)
{
writeln("Snapshot created: ID=", snap.id, ", Label=", snap.label);
}
// Restore data
auto snapshots = storage.getSnapshots("example_file");
if (snapshots.length > 0)
{
auto lastSnap = snapshots[0];
File outFile = File("restored.txt", "wb");
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
outFile.close();
writeln("Data restored to restored.txt");
}
// Delete snapshots
long deleted = storage.removeSnapshots("example_file");
writeln("Deleted snapshots: ", deleted);
}
```
## Tools
The `tools` directory contains a small D script for generating a Gear table used by FastCDC. It lets you build custom hash tables to tune splitting behavior. To generate a new table:
```bash ```bash
# Статическая библиотека chmod +x ./tools/gen.d
dub build -c static ./tools/gen.d > ./source/gear.d
# Динамическая библиотека
dub build -c dynamic
# Тест-утилита
dub build -c binary
``` ```
## Installation
* **In `dub.json`**:
```json
"dependencies": {
"cdcdb": "~>0.1.0"
}
```
* **Build**: `dub build`.
## License
Boost Software License 1.0 (BSL-1.0).

98
README.ru.md Normal file
View file

@ -0,0 +1,98 @@
# cdcdb
Библиотека для хранения и управления снимками текстовых данных в базе SQLite. Использует механизм content-defined chunking (CDC) на основе алгоритма FastCDC для разделения данных на чанки переменного размера, что обеспечивает эффективную дедупликацию. Поддерживает опциональную компрессию Zstd, транзакции и проверку целостности данных через SHA-256. Основное применение — резервное копирование и версионирование текстовых файлов с минимизацией занимаемого пространства.
## Алгоритм FastCDC
FastCDC — это алгоритм разделения данных на чанки переменного размера, основанный на хэшировании содержимого. Он использует таблицу Gear для вычисления "отпечатков" данных, определяя точки разделения с учетом минимального, целевого и максимального размеров чанка. Это позволяет эффективно выявлять изменения в данных и хранить только уникальные чанки, снижая объем хранилища.
## Основные классы
### Storage
Класс для работы с базой SQLite и управления снимками.
- **Конструктор**: Инициализирует подключение к базе SQLite.
- **Методы**:
- `newSnapshot`: Создает снимок данных. Возвращает объект `Snapshot` или `null`, если данные совпадают с последним снимком.
- `getSnapshots`: Получает список снимков (все или по метке). Возвращает массив объектов `Snapshot`.
- `getSnapshot`: Получает снимок по ID. Возвращает объект `Snapshot`.
- `setupCDC`: Настраивает параметры разделения данных CDC. Ничего не возвращает.
- `getVersion`: Возвращает строку с версией библиотеки (например, "0.0.2").
- `removeSnapshots`: Удаляет снимки по метке, ID или объекту Snapshot. Возвращает количество удаленных снимков (для метки) или `true`/`false` (для ID или объекта).
### Snapshot
Класс для работы с отдельным снимком данных.
- **Конструктор**: Создает объект снимка для работы с данными по его ID.
- **Методы**:
- `data`: Извлекает полные данные снимка. Возвращает массив байтов (`ubyte[]`).
- `data`: Извлекает данные снимка потоково через делегат. Ничего не возвращает.
- `remove`: Удаляет снимок из базы. Возвращает `true` при успешном удалении, иначе `false`.
- **Свойства**:
- `id`: ID снимка (long).
- `label`: Метка снимка (string).
- `created`: Временная метка создания (UTC, `DateTime`).
- `length`: Длина исходных данных (long).
- `sha256`: Хэш SHA-256 данных (ubyte[32]).
- `status`: Статус снимка ("pending" или "ready").
- `description`: Описание снимка (string).
## Пример использования
```d
import cdcdb;
import std.stdio : writeln, File;
import std.file : exists, remove;
void main()
{
// Создаем базу
string dbPath = "example.db";
// Инициализация Storage с компрессией Zstd
auto storage = new Storage(dbPath, true, 22);
// Создание снимка
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
auto snap = storage.newSnapshot("example_file", data, "Версия 1.0");
if (snap)
{
writeln("Создан снимок: ID=", snap.id, ", Метка=", snap.label);
}
// Восстановление данных
auto snapshots = storage.getSnapshots("example_file");
if (snapshots.length > 0)
{
auto lastSnap = snapshots[0];
File outFile = File("restored.txt", "wb");
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
outFile.close();
writeln("Данные восстановлены в restored.txt");
}
// Удаление снимков
long deleted = storage.removeSnapshots("example_file");
writeln("Удалено снимков: ", deleted);
}
```
## Инструменты
В директории `tools` находится скрипт на D для создания таблицы Gear, используемой в алгоритме FastCDC. Он позволяет генерировать пользовательские таблицы хэшей для настройки разделения данных. Для создания новой таблицы выполните:
```bash
chmod +x ./tools/gen.d
./tools/gen.d > ./source/gear.d
```
## Установка
- **В `dub.json`**:
```json
"dependencies": {
"cdcdb": "~>0.1.0"
}
```
- **Сборка**: `dub build`.
## Лицензия
Boost Software License 1.0 (BSL-1.0).

View file

@ -11,28 +11,48 @@
"zstd": "~>0.2.1" "zstd": "~>0.2.1"
}, },
"stringImportPaths": [ "stringImportPaths": [
"source/cdcdb/db", "source/cdcdb"
"source/cdcdb/cdc"
], ],
"configurations": [ "configurations": [
{ {
"name": "static", "name": "static",
"targetType": "staticLibrary", "targetType": "staticLibrary",
"targetPath": "lib", "targetPath": "lib",
"sourcePaths": ["source"] "sourcePaths": [
"source"
]
}, },
{ {
"name": "dynamic", "name": "dynamic",
"targetType": "dynamicLibrary", "targetType": "dynamicLibrary",
"targetPath": "lib", "targetPath": "lib",
"sourcePaths": ["source"] "sourcePaths": [
"source"
]
}, },
{ {
"name": "binary", "name": "binary",
"targetType": "executable", "targetType": "executable",
"targetPath": "bin", "targetPath": "bin",
"mainSourceFile": "test/app.d", "mainSourceFile": "test/app.d",
"sourcePaths": ["source", "test"] "sourcePaths": [
"source",
"test"
]
},
{
"name": "unittest",
"targetType": "executable",
"targetPath": "bin",
"sourcePaths": [
"source",
"test"
],
"buildOptions": [
"unittests"
],
"dflags": ["-main"],
"mainSourceFile": "test/unittest.d"
} }
] ]
} }

View file

@ -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;
}
}

View file

@ -1,3 +0,0 @@
module cdcdb.cdc;
public import cdcdb.cdc.cas;

View file

@ -1,4 +1,4 @@
module cdcdb.cdc.core; module cdcdb.core;
import std.digest.sha : SHA256, digest; import std.digest.sha : SHA256, digest;

View file

@ -1,4 +0,0 @@
module cdcdb.db;
public import cdcdb.db.dblite;
public import cdcdb.db.types;

View file

@ -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
```

View file

@ -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;
}

View file

@ -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.datetime : DateTime;
import std.exception : enforce;
import std.conv : to;
import std.string : join, replace, toLower; import std.string : join, replace, toLower;
import std.algorithm : canFind; import std.algorithm : canFind;
import std.conv : to;
import std.format : format; 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 final class DBLite : Sqlite
{ {
@ -120,7 +172,25 @@ public:
sql("ROLLBACK"); 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( auto queryResult = sql(
q{ q{
@ -157,13 +227,14 @@ public:
return queryResult.front()["id"].to!long; return queryResult.front()["id"].to!long;
} }
void addBlob(Blob blob) bool addBlob(DBBlob blob)
{ {
sql( auto queryResult = sql(
q{ q{
INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd) INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd)
VALUES (?,?,?,?,?,?) VALUES (?,?,?,?,?,?)
ON CONFLICT (sha256) DO NOTHING ON CONFLICT (sha256) DO NOTHING
RETURNING sha256
}, },
blob.sha256[], blob.sha256[],
blob.zstd ? blob.zSha256[] : null, blob.zstd ? blob.zSha256[] : null,
@ -172,76 +243,28 @@ public:
blob.content, blob.content,
blob.zstd.to!int blob.zstd.to!int
); );
return !queryResult.empty();
} }
void addSnapshotChunk(SnapshotChunk snapshotChunk) bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
{ {
sql( auto queryResult = sql(
q{ q{
INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256) INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256)
VALUES(?,?,?,?) VALUES(?,?,?,?)
RETURNING snapshot_id
}, },
snapshotChunk.snapshotId, snapshotChunk.snapshotId,
snapshotChunk.chunkIndex, snapshotChunk.chunkIndex,
snapshotChunk.offset, snapshotChunk.offset,
snapshotChunk.sha256[] snapshotChunk.sha256[]
); );
return !queryResult.empty();
} }
bool isLast(string label, ubyte[] sha256) { DBSnapshot getSnapshot(long id)
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( auto queryResult = sql(
q{ q{
@ -251,7 +274,7 @@ public:
}, id }, id
); );
Snapshot snapshot; DBSnapshot snapshot;
if (!queryResult.empty()) if (!queryResult.empty())
{ {
@ -274,11 +297,42 @@ public:
return snapshot; return snapshot;
} }
void deleteSnapshot(long id) { DBSnapshot[] getSnapshots(string label)
sql("DELETE FROM snapshots WHERE id = ?", id); {
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;
} }
SnapshotDataChunk[] getChunks(long snapshotId) return snapshots;
}
DBSnapshotChunkData[] getChunks(long snapshotId)
{ {
auto queryResult = sql( auto queryResult = sql(
q{ q{
@ -291,11 +345,11 @@ public:
}, snapshotId }, snapshotId
); );
SnapshotDataChunk[] sdchs; DBSnapshotChunkData[] sdchs;
foreach (row; queryResult) foreach (row; queryResult)
{ {
SnapshotDataChunk sdch; DBSnapshotChunkData sdch;
sdch.chunkIndex = row["chunk_index"].to!long; sdch.chunkIndex = row["chunk_index"].to!long;
sdch.offset = row["offset"].to!long; sdch.offset = row["offset"].to!long;
@ -311,4 +365,28 @@ public:
return sdchs; return sdchs;
} }
long deleteSnapshot(long id) {
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
if (!queryResult.empty()) {
return queryResult.front()["id"].to!long;
}
return 0;
}
long deleteSnapshot(string label) {
auto queryResult = sql(
q{
DELETE FROM snapshots
WHERE label = ?
RETURNING 1;
}, label);
if (!queryResult.empty()) {
return queryResult.length.to!long;
}
return 0;
}
} }

View file

@ -1,3 +1,4 @@
module cdcdb; module cdcdb;
public import cdcdb.cdc; public import cdcdb.storage;
public import cdcdb.snapshot;

142
source/cdcdb/snapshot.d Normal file
View file

@ -0,0 +1,142 @@
module cdcdb.snapshot;
import cdcdb.dblite;
import zstd : uncompress;
import std.digest.sha : SHA256, digest;
import std.datetime : DateTime;
import std.exception : enforce;
final class Snapshot
{
private:
DBLite _db;
DBSnapshot _snapshot;
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
{
ubyte[] bytes;
if (chunk.zstd)
{
enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
bytes = cast(ubyte[]) uncompress(chunk.content);
}
else
{
bytes = chunk.content.dup;
}
enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш-сумма фрагмента не совпадает");
return bytes;
}
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;
content.reserve(_snapshot.sourceLength);
auto fctx = SHA256();
foreach (chunk; chunks)
{
const(ubyte)[] bytes = getBytes(chunk);
content ~= bytes;
fctx.put(bytes);
}
enforce(_snapshot.sha256 == fctx.finish(), "Хеш-сумма файла не совпадает");
return content;
}
void data(void delegate(const(ubyte)[]) sink)
{
auto chunks = _db.getChunks(_snapshot.id);
auto fctx = SHA256();
foreach (chunk; chunks)
{
const(ubyte)[] bytes = getBytes(chunk);
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;
}
}

173
source/cdcdb/storage.d Normal file
View file

@ -0,0 +1,173 @@
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;
}
// Удаляет снимок по метке, возвращает количество удаленных снимков
long removeSnapshots(string label) {
return _db.deleteSnapshot(label);
}
bool removeSnapshots(Snapshot snapshot) {
return removeSnapshots(snapshot.id);
}
bool removeSnapshots(long idSnapshot) {
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
}
Snapshot getSnapshot(long idSnapshot) {
return new Snapshot(_db, idSnapshot);
}
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;
}
}

View file

@ -1,3 +1,3 @@
module cdcdb.version_; module cdcdb.version_;
enum cdcdbVersion = "0.0.1"; enum cdcdbVersion = "0.1.0";

View file

@ -1,18 +1,69 @@
import std.stdio;
import cdcdb; import cdcdb;
import std.stdio : writeln, File;
import std.file : read; import std.file : exists, remove, read;
import zstd : Level;
void main() void main()
{ {
auto cas = new CAS("/tmp/base.db", true); // Создаем временную базу для примера
cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text")); string dbPath = "./bin/example.db";
// import std.stdio : writeln;
foreach (snapshot; cas.getSnapshots()) { // Инициализация Storage с компрессией Zstd
writeln(snapshot); auto storage = new Storage(dbPath, true, Level.speed);
// Настройка параметров CDC (опционально)
storage.setupCDC(256, 512, 1024, 0xFF, 0x0F);
// Тестовые данные
ubyte[] data1 = cast(ubyte[]) "Hello, cdcdb!".dup;
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
// Создание первого снимка
auto snap1 = storage.newSnapshot("example_file", data1, "Версия 1.0");
if (snap1)
{
writeln("Создан снимок с ID: ", snap1.id);
writeln("Метка: ", snap1.label);
writeln("Размер: ", snap1.length, " байт");
writeln("Статус: ", snap1.status);
} }
// writeln(cas.getVersion); // Создание второго снимка (обновление)
auto snap2 = storage.newSnapshot("example_file", data2, "Версия 2.0");
if (snap2)
{
writeln("Создан обновленный снимок с ID: ", snap2.id);
}
// Получение всех снимков по метке
auto snapshots = storage.getSnapshots("example_file");
writeln("Найдено снимков: ", snapshots.length);
// Восстановление данных из последнего снимка (потоково, для экономии памяти)
if (snapshots.length > 0)
{
auto lastSnap = snapshots[$ - 1]; // Последний снимок
File outFile = File("./bin/restored.txt", "wb");
lastSnap.data((const(ubyte)[] chunk) { outFile.rawWrite(chunk); });
outFile.close();
writeln("Данные восстановлены в restored.txt");
// Проверка хэша (опционально)
import std.digest.sha : digest, SHA256;
auto restoredData = cast(ubyte[]) read("./bin/restored.txt");
assert(restoredData == data2);
writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData));
}
// Удаление снимков по метке
long deleted = storage.removeSnapshots("example_file");
writeln("Удалено снимков: ", deleted);
// Проверка: снимки удалены
auto remaining = storage.getSnapshots("example_file");
assert(remaining.length == 0);
writeln("Все снимки удалены.");
writeln("Версия библиотеки: ", storage.getVersion());
} }

72
test/unittest.d Normal file
View file

@ -0,0 +1,72 @@
import cdcdb;
import std.file : read, write, remove, exists;
import std.path : buildPath;
import std.digest.sha : digest, SHA256;
import std.exception : assertThrown, assertNotThrown;
import std.datetime : DateTime;
import core.thread : Thread;
import core.time : msecs, seconds;
unittest
{
const string dbPath = "./bin/test_cdcdb.db";
if (exists(dbPath)) {
remove(dbPath);
}
// Тест конструктора Storage
auto storage = new Storage(dbPath, true, 22);
// Тест настройки CDC
storage.setupCDC(128, 256, 512, 0xFF, 0x0F);
// Тест создания снимка
ubyte[] data1 = cast(ubyte[]) "Hello, World!".dup;
auto snap1 = storage.newSnapshot("test_label", data1, "First snapshot");
assert(snap1 !is null);
assert(snap1.label == "test_label");
assert(snap1.length == data1.length);
assert(snap1.sha256 == digest!SHA256(data1));
assert(snap1.status == "ready");
assert(snap1.description == "First snapshot");
// Тест дубликата (не должен создать новый)
auto snapDup = storage.newSnapshot("test_label", data1);
assert(snapDup is null);
// Тест изменения данных
ubyte[] data2 = cast(ubyte[]) "Hello, Changed World!".dup;
auto snap2 = storage.newSnapshot("test_label", data2);
assert(snap2 !is null);
assert(snap2.sha256 == digest!SHA256(data2));
// Тест восстановления данных
auto restored = snap1.data();
assert(restored == data1);
bool streamedOk = false;
snap2.data((const(ubyte)[] chunk) {
assert(chunk == data2); // Поскольку маленький файл — один фрагмент
streamedOk = true;
});
assert(streamedOk);
// Тест getSnapshots
auto snaps = storage.getSnapshots("test_label");
assert(snaps.length == 2);
assert(snaps[0].id == snap1.id);
assert(snaps[1].id == snap2.id);
auto allSnaps = storage.getSnapshots();
assert(allSnaps.length == 2);
// Тест удаления
assert(snap1.remove());
snaps = storage.getSnapshots("test_label");
assert(snaps.length == 1);
assert(snaps[0].id == snap2.id);
// Тест пустых данных
assertThrown!Exception(storage.newSnapshot("empty", []));
}