forked from dlang/cdcdb
Переписана под ООП
This commit is contained in:
parent
46138c032a
commit
8a9142234e
14 changed files with 372 additions and 526 deletions
3
dub.json
3
dub.json
|
@ -11,8 +11,7 @@
|
||||||
"zstd": "~>0.2.1"
|
"zstd": "~>0.2.1"
|
||||||
},
|
},
|
||||||
"stringImportPaths": [
|
"stringImportPaths": [
|
||||||
"source/cdcdb/db",
|
"source/cdcdb"
|
||||||
"source/cdcdb/cdc"
|
|
||||||
],
|
],
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
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.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
SnapshotDataChunk[] getChunks(long snapshotId)
|
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,14 @@ public:
|
||||||
|
|
||||||
return sdchs;
|
return sdchs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long deleteSnapshot(long id) {
|
||||||
|
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
|
||||||
|
|
||||||
|
if (queryResult.empty()) {
|
||||||
|
throw new Exception("Ошибка при удалении снимка из базы данных");
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryResult.front()["id"].to!long;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
module cdcdb;
|
module cdcdb;
|
||||||
|
|
||||||
public import cdcdb.cdc;
|
public import cdcdb.storage;
|
||||||
|
public import cdcdb.snapshot;
|
||||||
|
|
68
source/cdcdb/snapshot.d
Normal file
68
source/cdcdb/snapshot.d
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
module cdcdb.snapshot;
|
||||||
|
|
||||||
|
import cdcdb.dblite;
|
||||||
|
|
||||||
|
import std.exception : enforce;
|
||||||
|
|
||||||
|
final class Snapshot {
|
||||||
|
private:
|
||||||
|
DBLite _db;
|
||||||
|
DBSnapshot _snapshot;
|
||||||
|
public:
|
||||||
|
this(DBLite dblite, DBSnapshot dbSnapshot) {
|
||||||
|
_db = dblite;
|
||||||
|
_snapshot = dbSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(DBLite dblite, long idSnapshot) {
|
||||||
|
_db = dblite;
|
||||||
|
_snapshot = _db.getSnapshot(idSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
ubyte[] data() {
|
||||||
|
auto dataChunks = _db.getChunks(_snapshot.id);
|
||||||
|
ubyte[] content;
|
||||||
|
|
||||||
|
import zstd : uncompress;
|
||||||
|
|
||||||
|
foreach (chunk; dataChunks) {
|
||||||
|
ubyte[] bytes;
|
||||||
|
if (chunk.zstd) {
|
||||||
|
enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
|
||||||
|
bytes = cast(ubyte[]) uncompress(chunk.content);
|
||||||
|
} else {
|
||||||
|
bytes = chunk.content;
|
||||||
|
}
|
||||||
|
enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
|
||||||
|
content ~= bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
import std.digest.sha : SHA256, digest;
|
||||||
|
|
||||||
|
enforce(_snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool remove() {
|
||||||
|
_db.beginImmediate();
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
scope (exit)
|
||||||
|
{
|
||||||
|
if (!ok)
|
||||||
|
_db.rollback();
|
||||||
|
}
|
||||||
|
scope (success)
|
||||||
|
{
|
||||||
|
_db.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
long idDeleted = _db.deleteSnapshot(_snapshot.id);
|
||||||
|
|
||||||
|
ok = true;
|
||||||
|
|
||||||
|
return _snapshot.id == idDeleted;
|
||||||
|
}
|
||||||
|
}
|
154
source/cdcdb/storage.d
Normal file
154
source/cdcdb/storage.d
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
module cdcdb.storage;
|
||||||
|
|
||||||
|
import cdcdb.dblite;
|
||||||
|
import cdcdb.core;
|
||||||
|
import cdcdb.snapshot;
|
||||||
|
|
||||||
|
final class Storage
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
// Параметры работы с базой данных
|
||||||
|
DBLite _db;
|
||||||
|
bool _zstd;
|
||||||
|
// Настройки CDC механизма
|
||||||
|
CDC _cdc;
|
||||||
|
size_t _minSize;
|
||||||
|
size_t _normalSize;
|
||||||
|
size_t _maxSize;
|
||||||
|
size_t _maskS;
|
||||||
|
size_t _maskL;
|
||||||
|
|
||||||
|
void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024,
|
||||||
|
size_t maskS = 0xFF, size_t maskL = 0x0F)
|
||||||
|
{
|
||||||
|
_minSize = minSize;
|
||||||
|
_normalSize = normalSize;
|
||||||
|
_maxSize = maxSize;
|
||||||
|
_maskS = maskS;
|
||||||
|
_maskL = maskL;
|
||||||
|
// CDC не хранит динамически выделенных данных, переинициализация безопасна
|
||||||
|
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
this(string database, bool zstd = false, size_t busyTimeout = 3000, size_t maxRetries = 3)
|
||||||
|
{
|
||||||
|
_db = new DBLite(database, busyTimeout, maxRetries);
|
||||||
|
_zstd = zstd;
|
||||||
|
initCDC();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, size_t maskS, size_t maskL)
|
||||||
|
{
|
||||||
|
initCDC(minSize, normalSize, maxSize, maskS, maskL);
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init)
|
||||||
|
{
|
||||||
|
if (data.length == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("Данные имеют нулевой размер");
|
||||||
|
}
|
||||||
|
|
||||||
|
import std.digest.sha : SHA256, digest;
|
||||||
|
|
||||||
|
ubyte[32] sha256 = digest!SHA256(data);
|
||||||
|
|
||||||
|
// Если последний снимок файла соответствует текущему состоянию
|
||||||
|
if (_db.isLast(label, sha256))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
DBSnapshot dbSnapshot;
|
||||||
|
|
||||||
|
dbSnapshot.label = label;
|
||||||
|
dbSnapshot.sha256 = sha256;
|
||||||
|
dbSnapshot.description = description;
|
||||||
|
dbSnapshot.sourceLength = data.length;
|
||||||
|
dbSnapshot.algoMin = _minSize;
|
||||||
|
dbSnapshot.algoNormal = _normalSize;
|
||||||
|
dbSnapshot.algoMax = _maxSize;
|
||||||
|
dbSnapshot.maskS = _maskS;
|
||||||
|
dbSnapshot.maskL = _maskL;
|
||||||
|
|
||||||
|
_db.beginImmediate();
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
scope (exit)
|
||||||
|
{
|
||||||
|
if (!ok)
|
||||||
|
_db.rollback();
|
||||||
|
}
|
||||||
|
scope (success)
|
||||||
|
{
|
||||||
|
_db.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto idSnapshot = _db.addSnapshot(dbSnapshot);
|
||||||
|
|
||||||
|
DBSnapshotChunk dbSnapshotChunk;
|
||||||
|
DBBlob dbBlob;
|
||||||
|
|
||||||
|
dbBlob.zstd = _zstd;
|
||||||
|
|
||||||
|
// Разбить на фрагменты
|
||||||
|
Chunk[] chunks = _cdc.split(data);
|
||||||
|
|
||||||
|
import zstd : compress;
|
||||||
|
|
||||||
|
// Запись фрагментов в БД
|
||||||
|
foreach (chunk; chunks)
|
||||||
|
{
|
||||||
|
dbBlob.sha256 = chunk.sha256;
|
||||||
|
dbBlob.size = chunk.size;
|
||||||
|
|
||||||
|
auto content = data[chunk.offset .. chunk.offset + chunk.size];
|
||||||
|
|
||||||
|
if (_zstd) {
|
||||||
|
ubyte[] zBytes = compress(content, 22);
|
||||||
|
size_t zSize = zBytes.length;
|
||||||
|
ubyte[32] zHash = digest!SHA256(zBytes);
|
||||||
|
|
||||||
|
dbBlob.zSize = zSize;
|
||||||
|
dbBlob.zSha256 = zHash;
|
||||||
|
dbBlob.content = zBytes;
|
||||||
|
} else {
|
||||||
|
dbBlob.content = content.dup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запись фрагментов
|
||||||
|
_db.addBlob(dbBlob);
|
||||||
|
|
||||||
|
dbSnapshotChunk.snapshotId = idSnapshot;
|
||||||
|
dbSnapshotChunk.chunkIndex = chunk.index;
|
||||||
|
dbSnapshotChunk.offset = chunk.offset;
|
||||||
|
dbSnapshotChunk.sha256 = chunk.sha256;
|
||||||
|
|
||||||
|
// Привязка фрагментов к снимку
|
||||||
|
_db.addSnapshotChunk(dbSnapshotChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true;
|
||||||
|
|
||||||
|
Snapshot snapshot = new Snapshot(_db, idSnapshot);
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot[] getSnapshots(string label = string.init) {
|
||||||
|
Snapshot[] snapshots;
|
||||||
|
|
||||||
|
foreach (snapshot; _db.getSnapshots(label)) {
|
||||||
|
snapshots ~= new Snapshot(_db, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
string getVersion() const @safe nothrow
|
||||||
|
{
|
||||||
|
import cdcdb.version_ : cdcdbVersion;
|
||||||
|
|
||||||
|
return cdcdbVersion;
|
||||||
|
}
|
||||||
|
}
|
16
test/app.d
16
test/app.d
|
@ -6,12 +6,18 @@ import std.file : read;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
auto cas = new CAS("/tmp/base.db", true);
|
auto storage = new Storage("/tmp/base.db", true);
|
||||||
cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
storage.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
||||||
// import std.stdio : writeln;
|
|
||||||
|
|
||||||
foreach (snapshot; cas.getSnapshots()) {
|
// if (snapshot !is null) {
|
||||||
writeln(snapshot);
|
// writeln(cast(string) snapshot.data);
|
||||||
|
// snapshot.remove();
|
||||||
|
// }
|
||||||
|
|
||||||
|
import std.stdio : writeln;
|
||||||
|
|
||||||
|
foreach (snapshot; storage.getSnapshots()) {
|
||||||
|
writeln(cast(string) snapshot.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeln(cas.getVersion);
|
// writeln(cas.getVersion);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue