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"
|
||||
},
|
||||
"stringImportPaths": [
|
||||
"source/cdcdb/db",
|
||||
"source/cdcdb/cdc"
|
||||
"source/cdcdb"
|
||||
],
|
||||
"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;
|
||||
|
|
@ -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,14 @@ public:
|
|||
|
||||
return sdchs;
|
||||
}
|
||||
|
||||
long deleteSnapshot(long id) {
|
||||
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
|
||||
|
||||
if (queryResult.empty()) {
|
||||
throw new Exception("Ошибка при удалении снимка из базы данных");
|
||||
}
|
||||
|
||||
return queryResult.front()["id"].to!long;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
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()
|
||||
{
|
||||
auto cas = new CAS("/tmp/base.db", true);
|
||||
cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
||||
// import std.stdio : writeln;
|
||||
auto storage = new Storage("/tmp/base.db", true);
|
||||
storage.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
||||
|
||||
foreach (snapshot; cas.getSnapshots()) {
|
||||
writeln(snapshot);
|
||||
// if (snapshot !is null) {
|
||||
// writeln(cast(string) snapshot.data);
|
||||
// snapshot.remove();
|
||||
// }
|
||||
|
||||
import std.stdio : writeln;
|
||||
|
||||
foreach (snapshot; storage.getSnapshots()) {
|
||||
writeln(cast(string) snapshot.data);
|
||||
}
|
||||
|
||||
// writeln(cas.getVersion);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue