Compare commits
5 commits
46138c032a
...
df1af87322
| Author | SHA1 | Date | |
|---|---|---|---|
| df1af87322 | |||
| 8639c36f46 | |||
| 8716a90463 | |||
| d93dc4d81b | |||
| 8a9142234e |
18 changed files with 820 additions and 543 deletions
104
README.md
104
README.md
|
|
@ -1,14 +1,102 @@
|
|||
# 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
|
||||
# Статическая библиотека
|
||||
dub build -c static
|
||||
# Динамическая библиотека
|
||||
dub build -c dynamic
|
||||
# Тест-утилита
|
||||
dub build -c binary
|
||||
chmod +x ./tools/gen.d
|
||||
./tools/gen.d > ./source/gear.d
|
||||
```
|
||||
|
||||
## 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
98
README.ru.md
Normal 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).
|
||||
30
dub.json
30
dub.json
|
|
@ -11,28 +11,48 @@
|
|||
"zstd": "~>0.2.1"
|
||||
},
|
||||
"stringImportPaths": [
|
||||
"source/cdcdb/db",
|
||||
"source/cdcdb/cdc"
|
||||
"source/cdcdb"
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"name": "static",
|
||||
"targetType": "staticLibrary",
|
||||
"targetPath": "lib",
|
||||
"sourcePaths": ["source"]
|
||||
"sourcePaths": [
|
||||
"source"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dynamic",
|
||||
"targetType": "dynamicLibrary",
|
||||
"targetPath": "lib",
|
||||
"sourcePaths": ["source"]
|
||||
"sourcePaths": [
|
||||
"source"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "binary",
|
||||
"targetType": "executable",
|
||||
"targetPath": "bin",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module cdcdb.cdc;
|
||||
|
||||
public import cdcdb.cdc.cas;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
module cdcdb.cdc.core;
|
||||
module cdcdb.core;
|
||||
|
||||
import std.digest.sha : SHA256, digest;
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
module cdcdb.db;
|
||||
|
||||
public import cdcdb.db.dblite;
|
||||
public import cdcdb.db.types;
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,28 @@ public:
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
module cdcdb;
|
||||
|
||||
public import cdcdb.cdc;
|
||||
public import cdcdb.storage;
|
||||
public import cdcdb.snapshot;
|
||||
|
|
|
|||
142
source/cdcdb/snapshot.d
Normal file
142
source/cdcdb/snapshot.d
Normal 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
173
source/cdcdb/storage.d
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
module cdcdb.version_;
|
||||
|
||||
enum cdcdbVersion = "0.0.1";
|
||||
enum cdcdbVersion = "0.1.0";
|
||||
|
|
|
|||
71
test/app.d
71
test/app.d
|
|
@ -1,18 +1,69 @@
|
|||
import std.stdio;
|
||||
|
||||
import cdcdb;
|
||||
|
||||
import std.file : read;
|
||||
import std.stdio : writeln, File;
|
||||
import std.file : exists, remove, read;
|
||||
import zstd : Level;
|
||||
|
||||
void main()
|
||||
{
|
||||
auto cas = new CAS("/tmp/base.db", true);
|
||||
cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
||||
// import std.stdio : writeln;
|
||||
// Создаем временную базу для примера
|
||||
string dbPath = "./bin/example.db";
|
||||
|
||||
foreach (snapshot; cas.getSnapshots()) {
|
||||
writeln(snapshot);
|
||||
// Инициализация Storage с компрессией Zstd
|
||||
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
72
test/unittest.d
Normal 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", []));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue