Обновлен CAS, добавлены проверки
This commit is contained in:
parent
9c5d812a49
commit
b29b328f91
4 changed files with 161 additions and 117 deletions
|
@ -14,12 +14,34 @@ import std.conv : to;
|
||||||
|
|
||||||
import std.file : write;
|
import std.file : write;
|
||||||
|
|
||||||
// CAS-хранилище (Content-Addressable Storage) со снапшотами
|
// Content-Addressable Storage (Контентно-адресуемая система хранения)
|
||||||
|
// CAS-хранилище со снапшотами
|
||||||
final class CAS
|
final class CAS
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
DBLite _db;
|
DBLite _db;
|
||||||
bool _zstd;
|
bool _zstd;
|
||||||
|
|
||||||
|
ubyte[] buildContent(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.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
public:
|
public:
|
||||||
this(string database, bool zstd = false)
|
this(string database, bool zstd = false)
|
||||||
{
|
{
|
||||||
|
@ -27,14 +49,12 @@ public:
|
||||||
_zstd = zstd;
|
_zstd = zstd;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t saveSnapshot(string filePath, const(ubyte)[] data)
|
size_t newSnapshot(string filePath, string label, const(ubyte)[] data)
|
||||||
{
|
{
|
||||||
ubyte[32] hashSource = digest!SHA256(data);
|
ubyte[32] hashSource = digest!SHA256(data);
|
||||||
// Сделать запрос в БД по filePath и сверить хеш файлов
|
// Сделать запрос в БД по filePath и сверить хеш файлов
|
||||||
|
|
||||||
// writeln(hashSource.length);
|
if (_db.isLast(filePath, hashSource)) return 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Параметры для CDC вынести в отдельные настройки (продумать)
|
// Параметры для CDC вынести в отдельные настройки (продумать)
|
||||||
auto cdc = new CDC(256, 512, 1024, 0xFF, 0x0F);
|
auto cdc = new CDC(256, 512, 1024, 0xFF, 0x0F);
|
||||||
|
@ -42,13 +62,26 @@ public:
|
||||||
auto chunks = cdc.split(data);
|
auto chunks = cdc.split(data);
|
||||||
|
|
||||||
Snapshot snapshot;
|
Snapshot snapshot;
|
||||||
|
|
||||||
snapshot.filePath = filePath;
|
snapshot.filePath = filePath;
|
||||||
snapshot.fileSha256 = hashSource;
|
snapshot.fileSha256 = hashSource;
|
||||||
snapshot.label = "Файл для теста";
|
snapshot.label = label;
|
||||||
snapshot.sourceLength = data.length;
|
snapshot.sourceLength = data.length;
|
||||||
|
|
||||||
_db.beginImmediate();
|
_db.beginImmediate();
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
scope (exit)
|
||||||
|
{
|
||||||
|
if (!ok)
|
||||||
|
_db.rollback();
|
||||||
|
}
|
||||||
|
scope (success)
|
||||||
|
{
|
||||||
|
_db.commit();
|
||||||
|
}
|
||||||
|
|
||||||
auto idSnapshot = _db.addSnapshot(snapshot);
|
auto idSnapshot = _db.addSnapshot(snapshot);
|
||||||
|
|
||||||
SnapshotChunk snapshotChunk;
|
SnapshotChunk snapshotChunk;
|
||||||
|
@ -56,7 +89,7 @@ public:
|
||||||
|
|
||||||
blob.zstd = _zstd;
|
blob.zstd = _zstd;
|
||||||
|
|
||||||
// Записать фрагменты в БД
|
// Запись фрагментов в БД
|
||||||
foreach (chunk; chunks)
|
foreach (chunk; chunks)
|
||||||
{
|
{
|
||||||
blob.sha256 = chunk.sha256;
|
blob.sha256 = chunk.sha256;
|
||||||
|
@ -76,6 +109,7 @@ public:
|
||||||
blob.content = content.dup;
|
blob.content = content.dup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Запись фрагментов
|
||||||
_db.addBlob(blob);
|
_db.addBlob(blob);
|
||||||
|
|
||||||
snapshotChunk.snapshotId = idSnapshot;
|
snapshotChunk.snapshotId = idSnapshot;
|
||||||
|
@ -83,38 +117,28 @@ public:
|
||||||
snapshotChunk.offset = chunk.offset;
|
snapshotChunk.offset = chunk.offset;
|
||||||
snapshotChunk.sha256 = chunk.sha256;
|
snapshotChunk.sha256 = chunk.sha256;
|
||||||
|
|
||||||
|
// Привязка фрагментов к снимку
|
||||||
_db.addSnapshotChunk(snapshotChunk);
|
_db.addSnapshotChunk(snapshotChunk);
|
||||||
}
|
}
|
||||||
_db.commit();
|
|
||||||
// Записать манифест в БД
|
ok = true;
|
||||||
// Вернуть ID манифеста
|
|
||||||
return 0;
|
return idSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreSnapshot()
|
Snapshot[] getSnapshotList(string filePath)
|
||||||
{
|
{
|
||||||
string restoreFile = "/tmp/restore.d";
|
return _db.getSnapshots(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Snapshot snapshot; _db.getSnapshots("/tmp/text")) {
|
ubyte[] getSnapshotData(const ref Snapshot snapshot)
|
||||||
auto dataChunks = _db.getChunks(snapshot.id);
|
{
|
||||||
ubyte[] content;
|
ubyte[] content = buildContent(snapshot);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (SnapshotDataChunk chunk; dataChunks) {
|
void removeSnapshot(const ref Snapshot snapshot)
|
||||||
ubyte[] bytes;
|
{
|
||||||
if (chunk.zstd) {
|
_db.deleteSnapshot(snapshot.id);
|
||||||
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.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
|
|
||||||
|
|
||||||
write(snapshot.filePath ~ snapshot.id.to!string, content);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import cdcdb.cdc.types;
|
||||||
|
|
||||||
import std.digest.sha : SHA256, digest;
|
import std.digest.sha : SHA256, digest;
|
||||||
|
|
||||||
// Change Data Capture
|
// Change Data Capture (Захват изменения данных)
|
||||||
final class CDC
|
final class CDC
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
module cdcdb.db.dblite;
|
module cdcdb.db.dblite;
|
||||||
|
|
||||||
|
import cdcdb.db.types;
|
||||||
|
|
||||||
import arsd.sqlite;
|
import arsd.sqlite;
|
||||||
import std.file : exists, isFile;
|
|
||||||
|
import std.file : exists;
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
import std.conv : to;
|
import std.conv : to;
|
||||||
|
import std.string : join;
|
||||||
import cdcdb.db.types;
|
|
||||||
|
|
||||||
final class DBLite : Sqlite
|
final class DBLite : Sqlite
|
||||||
{
|
{
|
||||||
|
@ -18,16 +20,50 @@ private:
|
||||||
{
|
{
|
||||||
return cast(SqliteResult) query(queryText, args);
|
return cast(SqliteResult) query(queryText, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка БД на наличие существующих в ней необходимых таблиц
|
||||||
|
void check()
|
||||||
|
{
|
||||||
|
SqliteResult queryResult = sql(
|
||||||
|
q{
|
||||||
|
WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"))
|
||||||
|
SELECT name AS missing_table
|
||||||
|
FROM required
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = "table" AND name = required.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
string[] missingTables;
|
||||||
|
|
||||||
|
foreach (row; queryResult)
|
||||||
|
{
|
||||||
|
missingTables ~= row["missing_table"].to!string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enforce(missingTables.length == 0 || missingTables.length == 3,
|
||||||
|
"База данных повреждена. Отсутствуют таблицы: " ~ missingTables.join(", ")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingTables.length == 3)
|
||||||
|
{
|
||||||
|
foreach (schemeQuery; _scheme)
|
||||||
|
{
|
||||||
|
sql(schemeQuery);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
this(string database)
|
this(string database)
|
||||||
{
|
{
|
||||||
_dbPath = database;
|
_dbPath = database;
|
||||||
super(database);
|
super(database);
|
||||||
|
|
||||||
foreach (schemeQuery; _scheme)
|
check();
|
||||||
{
|
|
||||||
sql(schemeQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
query("PRAGMA journal_mode=WAL");
|
query("PRAGMA journal_mode=WAL");
|
||||||
query("PRAGMA synchronous=NORMAL");
|
query("PRAGMA synchronous=NORMAL");
|
||||||
|
@ -49,16 +85,6 @@ public:
|
||||||
query("ROLLBACK");
|
query("ROLLBACK");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot getSnapshot(string filePath, immutable ubyte[32] sha256)
|
|
||||||
// {
|
|
||||||
// auto queryResult = sql(
|
|
||||||
// q{
|
|
||||||
// SELECT * FROM snapshots
|
|
||||||
// WHERE file_path = ? AND file_sha256 = ?
|
|
||||||
// }, filePath, sha256
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
long addSnapshot(Snapshot snapshot)
|
long addSnapshot(Snapshot snapshot)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
|
@ -125,66 +151,23 @@ public:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct ChunkInput
|
bool isLast(string filePath, ubyte[] fileSha256) {
|
||||||
// {
|
auto queryResult = sql(
|
||||||
// long index;
|
q{
|
||||||
// long offset;
|
SELECT COALESCE(
|
||||||
// long size;
|
(SELECT (file_path = ? AND file_sha256 = ?)
|
||||||
// ubyte[32] sha256;
|
FROM snapshots
|
||||||
// const(ubyte)[] content;
|
ORDER BY created_utc DESC
|
||||||
// }
|
LIMIT 1),
|
||||||
|
0
|
||||||
|
) AS is_last;
|
||||||
|
}, filePath, fileSha256
|
||||||
|
);
|
||||||
|
|
||||||
// long saveSnapshotWithChunks(
|
if (!queryResult.empty())
|
||||||
// string filePath, string label, long sourceLength,
|
return queryResult.front()["is_last"].to!long > 0;
|
||||||
// long algoMin, long algoNormal, long algoMax,
|
return false;
|
||||||
// long maskS, long maskL,
|
}
|
||||||
// const ChunkInput[] chunks
|
|
||||||
// )
|
|
||||||
// {
|
|
||||||
// beginImmediate();
|
|
||||||
|
|
||||||
// bool ok;
|
|
||||||
|
|
||||||
// scope (exit)
|
|
||||||
// {
|
|
||||||
// if (!ok)
|
|
||||||
// rollback();
|
|
||||||
// }
|
|
||||||
// scope (success)
|
|
||||||
// {
|
|
||||||
// commit();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const snapId = insertSnapshotMeta(
|
|
||||||
// filePath, label, sourceLength,
|
|
||||||
// algoMin, algoNormal, algoMax,
|
|
||||||
// maskS, maskL, SnapshotStatus.pending
|
|
||||||
// );
|
|
||||||
|
|
||||||
// foreach (c; chunks)
|
|
||||||
// {
|
|
||||||
// insertBlobIfMissing(c.sha256, c.size, c.content);
|
|
||||||
// insertSnapshotChunk(snapId, c.index, c.offset, c.size, c.sha256);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ok = true;
|
|
||||||
|
|
||||||
// return snapId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// // --- чтение ---
|
|
||||||
|
|
||||||
Snapshot[] getSnapshots(string filePath)
|
Snapshot[] getSnapshots(string filePath)
|
||||||
{
|
{
|
||||||
|
@ -197,7 +180,7 @@ public:
|
||||||
);
|
);
|
||||||
|
|
||||||
Snapshot[] snapshots;
|
Snapshot[] snapshots;
|
||||||
// bool found = false;
|
|
||||||
foreach (row; queryResult)
|
foreach (row; queryResult)
|
||||||
{
|
{
|
||||||
Snapshot snapshot;
|
Snapshot snapshot;
|
||||||
|
@ -213,15 +196,53 @@ public:
|
||||||
snapshot.algoMax = row["algo_max"].to!long;
|
snapshot.algoMax = row["algo_max"].to!long;
|
||||||
snapshot.maskS = row["mask_s"].to!long;
|
snapshot.maskS = row["mask_s"].to!long;
|
||||||
snapshot.maskL = row["mask_l"].to!long;
|
snapshot.maskL = row["mask_l"].to!long;
|
||||||
snapshot.status = cast(SnapshotStatus)row["status"].to!int;
|
snapshot.status = cast(SnapshotStatus) row["status"].to!int;
|
||||||
// found = true;
|
|
||||||
snapshots ~= snapshot;
|
snapshots ~= snapshot;
|
||||||
}
|
}
|
||||||
// enforce(found, "getSnapshot: not found");
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
SnapshotDataChunk[] getChunks(long snapshotId) {
|
Snapshot getSnapshot(long id)
|
||||||
|
{
|
||||||
|
auto queryResult = sql(
|
||||||
|
q{
|
||||||
|
SELECT id, file_path, file_sha256, label, created_utc, source_length,
|
||||||
|
algo_min, algo_normal, algo_max, mask_s, mask_l, status
|
||||||
|
FROM snapshots WHERE id = ?
|
||||||
|
}, id
|
||||||
|
);
|
||||||
|
|
||||||
|
Snapshot snapshot;
|
||||||
|
|
||||||
|
if (!queryResult.empty())
|
||||||
|
{
|
||||||
|
auto data = queryResult.front();
|
||||||
|
|
||||||
|
snapshot.id = data["id"].to!long;
|
||||||
|
snapshot.filePath = data["file_path"].to!string;
|
||||||
|
snapshot.fileSha256 = cast(ubyte[]) data["file_sha256"].dup;
|
||||||
|
snapshot.label = data["label"].to!string;
|
||||||
|
snapshot.createdUtc = data["created_utc"].to!string;
|
||||||
|
snapshot.sourceLength = data["source_length"].to!long;
|
||||||
|
snapshot.algoMin = data["algo_min"].to!long;
|
||||||
|
snapshot.algoNormal = data["algo_normal"].to!long;
|
||||||
|
snapshot.algoMax = data["algo_max"].to!long;
|
||||||
|
snapshot.maskS = data["mask_s"].to!long;
|
||||||
|
snapshot.maskL = data["mask_l"].to!long;
|
||||||
|
snapshot.status = cast(SnapshotStatus) data["status"].to!int;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteSnapshot(long id) {
|
||||||
|
sql("DELETE FROM snapshots WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapshotDataChunk[] getChunks(long snapshotId)
|
||||||
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT sc.chunk_index, sc.offset,
|
SELECT sc.chunk_index, sc.offset,
|
||||||
|
|
|
@ -7,6 +7,5 @@ import std.file : read;
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
auto cas = new CAS("/tmp/base.db", true);
|
auto cas = new CAS("/tmp/base.db", true);
|
||||||
cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
cas.newSnapshot("/tmp/text", "Файл для тестирования", cast(ubyte[]) read("/tmp/text"));
|
||||||
// cas.restoreSnapshot();
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue