Compare commits
2 commits
c8d21bc3ce
...
9ba43b0e38
Author | SHA1 | Date | |
---|---|---|---|
9ba43b0e38 | |||
4f735abae7 |
5 changed files with 165 additions and 70 deletions
|
@ -22,51 +22,54 @@ private:
|
||||||
DBLite _db;
|
DBLite _db;
|
||||||
bool _zstd;
|
bool _zstd;
|
||||||
|
|
||||||
ubyte[] buildContent(const ref Snapshot snapshot)
|
size_t _minSize;
|
||||||
{
|
size_t _normalSize;
|
||||||
auto dataChunks = _db.getChunks(snapshot.id);
|
size_t _maxSize;
|
||||||
ubyte[] content;
|
size_t _maskS;
|
||||||
|
size_t _maskL;
|
||||||
foreach (chunk; dataChunks) {
|
CDC _cdc;
|
||||||
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,
|
||||||
_db = new DBLite(database);
|
bool zstd = false,
|
||||||
|
int busyTimeout = 3000,
|
||||||
|
ubyte 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;
|
_zstd = zstd;
|
||||||
|
|
||||||
|
_minSize = minSize;
|
||||||
|
_normalSize = normalSize;
|
||||||
|
_maxSize = maxSize;
|
||||||
|
_maskS = maskS;
|
||||||
|
_maskL = maskL;
|
||||||
|
|
||||||
|
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t newSnapshot(string filePath, string label, const(ubyte)[] data)
|
size_t newSnapshot(string label, const(ubyte)[] data, string description = string.init)
|
||||||
{
|
{
|
||||||
ubyte[32] hashSource = digest!SHA256(data);
|
ubyte[32] sha256 = digest!SHA256(data);
|
||||||
// Сделать запрос в БД по filePath и сверить хеш файлов
|
|
||||||
|
|
||||||
if (_db.isLast(filePath, hashSource)) return 0;
|
// Если последний снимок файла соответствует текущему состоянию
|
||||||
|
if (_db.isLast(label, sha256)) return 0;
|
||||||
// Параметры для CDC вынести в отдельные настройки (продумать)
|
|
||||||
auto cdc = new CDC(256, 512, 1024, 0xFF, 0x0F);
|
|
||||||
// Разбить на фрагменты
|
|
||||||
auto chunks = cdc.split(data);
|
|
||||||
|
|
||||||
Snapshot snapshot;
|
Snapshot snapshot;
|
||||||
|
|
||||||
snapshot.filePath = filePath;
|
|
||||||
snapshot.fileSha256 = hashSource;
|
|
||||||
snapshot.label = label;
|
snapshot.label = label;
|
||||||
|
snapshot.sha256 = sha256;
|
||||||
|
snapshot.description = description;
|
||||||
snapshot.sourceLength = data.length;
|
snapshot.sourceLength = data.length;
|
||||||
|
snapshot.algoMin = _minSize;
|
||||||
|
snapshot.algoNormal = _normalSize;
|
||||||
|
snapshot.algoMax = _maxSize;
|
||||||
|
snapshot.maskS = _maskS;
|
||||||
|
snapshot.maskL = _maskL;
|
||||||
|
|
||||||
_db.beginImmediate();
|
_db.beginImmediate();
|
||||||
|
|
||||||
|
@ -89,6 +92,9 @@ public:
|
||||||
|
|
||||||
blob.zstd = _zstd;
|
blob.zstd = _zstd;
|
||||||
|
|
||||||
|
// Разбить на фрагменты
|
||||||
|
auto chunks = _cdc.split(data);
|
||||||
|
|
||||||
// Запись фрагментов в БД
|
// Запись фрагментов в БД
|
||||||
foreach (chunk; chunks)
|
foreach (chunk; chunks)
|
||||||
{
|
{
|
||||||
|
@ -133,7 +139,22 @@ public:
|
||||||
|
|
||||||
ubyte[] getSnapshotData(const ref Snapshot snapshot)
|
ubyte[] getSnapshotData(const ref Snapshot snapshot)
|
||||||
{
|
{
|
||||||
ubyte[] content = buildContent(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;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,45 @@ import arsd.sqlite;
|
||||||
import std.file : exists;
|
import std.file : exists;
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
import std.conv : to;
|
import std.conv : to;
|
||||||
import std.string : join, replace;
|
import std.string : join, replace, toLower;
|
||||||
|
import std.algorithm : canFind;
|
||||||
|
import std.format : format;
|
||||||
|
|
||||||
final class DBLite : Sqlite
|
final class DBLite : Sqlite
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
string _dbPath;
|
string _dbPath;
|
||||||
|
ubyte _maxRetries;
|
||||||
// _scheme
|
// _scheme
|
||||||
mixin(import("scheme.d"));
|
mixin(import("scheme.d"));
|
||||||
|
|
||||||
SqliteResult sql(T...)(string queryText, T args)
|
SqliteResult sql(T...)(string queryText, T args)
|
||||||
{
|
{
|
||||||
return cast(SqliteResult) query(queryText, args);
|
if (_maxRetries == 0) {
|
||||||
|
return cast(SqliteResult) query(queryText, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
string msg;
|
||||||
|
ubyte tryNo = _maxRetries;
|
||||||
|
|
||||||
|
while (tryNo) {
|
||||||
|
try {
|
||||||
|
return cast(SqliteResult) query(queryText, args);
|
||||||
|
} catch (DatabaseException e) {
|
||||||
|
msg = e.msg;
|
||||||
|
if (msg.toLower.canFind("locked", "busy")) {
|
||||||
|
if (--tryNo == 0) {
|
||||||
|
throw new Exception(
|
||||||
|
"Не удалось выполнить подключение к базе данных после %d неудачных попыток: %s"
|
||||||
|
.format(_maxRetries, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Exception(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверка БД на наличие существующих в ней необходимых таблиц
|
// Проверка БД на наличие существующих в ней необходимых таблиц
|
||||||
|
@ -64,31 +91,34 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
this(string database)
|
this(string database, int busyTimeout, ubyte maxRetries)
|
||||||
{
|
{
|
||||||
_dbPath = database;
|
_dbPath = database;
|
||||||
super(database);
|
super(database);
|
||||||
|
|
||||||
check();
|
check();
|
||||||
|
|
||||||
|
_maxRetries = maxRetries;
|
||||||
|
|
||||||
query("PRAGMA journal_mode=WAL");
|
query("PRAGMA journal_mode=WAL");
|
||||||
query("PRAGMA synchronous=NORMAL");
|
query("PRAGMA synchronous=NORMAL");
|
||||||
query("PRAGMA foreign_keys=ON");
|
query("PRAGMA foreign_keys=ON");
|
||||||
|
query("PRAGMA busy_timeout=%d".format(busyTimeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
void beginImmediate()
|
void beginImmediate()
|
||||||
{
|
{
|
||||||
query("BEGIN IMMEDIATE");
|
sql("BEGIN IMMEDIATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
void commit()
|
void commit()
|
||||||
{
|
{
|
||||||
query("COMMIT");
|
sql("COMMIT");
|
||||||
}
|
}
|
||||||
|
|
||||||
void rollback()
|
void rollback()
|
||||||
{
|
{
|
||||||
query("ROLLBACK");
|
sql("ROLLBACK");
|
||||||
}
|
}
|
||||||
|
|
||||||
long addSnapshot(Snapshot snapshot)
|
long addSnapshot(Snapshot snapshot)
|
||||||
|
@ -96,9 +126,9 @@ public:
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
INSERT INTO snapshots(
|
INSERT INTO snapshots(
|
||||||
file_path,
|
|
||||||
file_sha256,
|
|
||||||
label,
|
label,
|
||||||
|
sha256,
|
||||||
|
description,
|
||||||
source_length,
|
source_length,
|
||||||
algo_min,
|
algo_min,
|
||||||
algo_normal,
|
algo_normal,
|
||||||
|
@ -109,9 +139,9 @@ public:
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
},
|
},
|
||||||
snapshot.filePath,
|
|
||||||
snapshot.fileSha256[],
|
|
||||||
snapshot.label,
|
snapshot.label,
|
||||||
|
snapshot.sha256[],
|
||||||
|
snapshot.description.length ? snapshot.description : null,
|
||||||
snapshot.sourceLength,
|
snapshot.sourceLength,
|
||||||
snapshot.algoMin,
|
snapshot.algoMin,
|
||||||
snapshot.algoNormal,
|
snapshot.algoNormal,
|
||||||
|
@ -157,17 +187,17 @@ public:
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isLast(string filePath, ubyte[] fileSha256) {
|
bool isLast(string label, ubyte[] sha256) {
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT COALESCE(
|
SELECT COALESCE(
|
||||||
(SELECT (file_path = ? AND file_sha256 = ?)
|
(SELECT (label = ? AND sha256 = ?)
|
||||||
FROM snapshots
|
FROM snapshots
|
||||||
ORDER BY created_utc DESC
|
ORDER BY created_utc DESC
|
||||||
LIMIT 1),
|
LIMIT 1),
|
||||||
0
|
0
|
||||||
) AS is_last;
|
) AS is_last;
|
||||||
}, filePath, fileSha256
|
}, label, sha256
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!queryResult.empty())
|
if (!queryResult.empty())
|
||||||
|
@ -175,14 +205,14 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Snapshot[] getSnapshots(string filePath)
|
Snapshot[] getSnapshots(string label)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT id, file_path, file_sha256, label, created_utc, source_length,
|
SELECT id, label, sha256, description, created_utc, source_length,
|
||||||
algo_min, algo_normal, algo_max, mask_s, mask_l, status
|
algo_min, algo_normal, algo_max, mask_s, mask_l, status
|
||||||
FROM snapshots WHERE file_path = ?
|
FROM snapshots WHERE label = ?
|
||||||
}, filePath
|
}, label
|
||||||
);
|
);
|
||||||
|
|
||||||
Snapshot[] snapshots;
|
Snapshot[] snapshots;
|
||||||
|
@ -192,9 +222,9 @@ public:
|
||||||
Snapshot snapshot;
|
Snapshot snapshot;
|
||||||
|
|
||||||
snapshot.id = row["id"].to!long;
|
snapshot.id = row["id"].to!long;
|
||||||
snapshot.filePath = row["file_path"].to!string;
|
|
||||||
snapshot.fileSha256 = cast(ubyte[]) row["file_sha256"].dup;
|
|
||||||
snapshot.label = row["label"].to!string;
|
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.createdUtc = toDateTime(row["created_utc"].to!string);
|
||||||
snapshot.sourceLength = row["source_length"].to!long;
|
snapshot.sourceLength = row["source_length"].to!long;
|
||||||
snapshot.algoMin = row["algo_min"].to!long;
|
snapshot.algoMin = row["algo_min"].to!long;
|
||||||
|
@ -214,7 +244,7 @@ public:
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT id, file_path, file_sha256, label, created_utc, source_length,
|
SELECT id, label, sha256, description, created_utc, source_length,
|
||||||
algo_min, algo_normal, algo_max, mask_s, mask_l, status
|
algo_min, algo_normal, algo_max, mask_s, mask_l, status
|
||||||
FROM snapshots WHERE id = ?
|
FROM snapshots WHERE id = ?
|
||||||
}, id
|
}, id
|
||||||
|
@ -227,9 +257,9 @@ public:
|
||||||
auto data = queryResult.front();
|
auto data = queryResult.front();
|
||||||
|
|
||||||
snapshot.id = data["id"].to!long;
|
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.label = data["label"].to!string;
|
||||||
|
snapshot.sha256 = cast(ubyte[]) data["sha256"].dup;
|
||||||
|
snapshot.description = data["description"].to!string;
|
||||||
snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
|
snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
|
||||||
snapshot.sourceLength = data["source_length"].to!long;
|
snapshot.sourceLength = data["source_length"].to!long;
|
||||||
snapshot.algoMin = data["algo_min"].to!long;
|
snapshot.algoMin = data["algo_min"].to!long;
|
||||||
|
|
|
@ -6,12 +6,12 @@ auto _scheme = [
|
||||||
CREATE TABLE IF NOT EXISTS snapshots (
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
-- идентификатор снимка
|
-- идентификатор снимка
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
-- путь к исходному файлу
|
|
||||||
file_path TEXT,
|
|
||||||
-- SHA-256 всего файла (BLOB(32))
|
|
||||||
file_sha256 BLOB NOT NULL CHECK (length(file_sha256) = 32),
|
|
||||||
-- метка/название снимка
|
-- метка/название снимка
|
||||||
label TEXT,
|
label TEXT NOT NULL,
|
||||||
|
-- SHA-256 всего файла (BLOB(32))
|
||||||
|
sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
|
||||||
|
-- Комментарий/описание
|
||||||
|
description TEXT DEFAULT NULL,
|
||||||
-- время создания (UTC)
|
-- время создания (UTC)
|
||||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
-- длина исходного файла в байтах
|
-- длина исходного файла в байтах
|
||||||
|
@ -88,9 +88,9 @@ auto _scheme = [
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для запросов вида: WHERE file_path=? AND file_sha256=?
|
-- Индекс для запросов вида: WHERE label=? AND sha256=?
|
||||||
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
||||||
ON snapshots(file_path, file_sha256)
|
ON snapshots(label, sha256)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для обратного поиска использования blob по sha256
|
-- Индекс для обратного поиска использования blob по sha256
|
||||||
|
@ -203,7 +203,51 @@ auto _scheme = [
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_sc_block_update
|
CREATE TRIGGER IF NOT EXISTS trg_sc_block_update
|
||||||
BEFORE UPDATE ON snapshot_chunks
|
BEFORE UPDATE ON snapshot_chunks
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT RAISE(ABORT, "snapshot_chunks: UPDATE запрещён; используйте DELETE + INSERT");
|
SELECT RAISE(ABORT, "snapshot_chunks: UPDATE запрещён");
|
||||||
|
END
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- snapshots: можно менять только status
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TRIGGER IF NOT EXISTS trg_snapshots_block_update
|
||||||
|
BEFORE UPDATE ON snapshots
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN
|
||||||
|
NEW.id IS NOT OLD.id OR
|
||||||
|
NEW.label IS NOT OLD.label OR
|
||||||
|
NEW.sha256 IS NOT OLD.sha256 OR
|
||||||
|
NEW.description IS NOT OLD.description OR
|
||||||
|
NEW.created_utc IS NOT OLD.created_utc OR
|
||||||
|
NEW.source_length IS NOT OLD.source_length OR
|
||||||
|
NEW.algo_min IS NOT OLD.algo_min OR
|
||||||
|
NEW.algo_normal IS NOT OLD.algo_normal OR
|
||||||
|
NEW.algo_max IS NOT OLD.algo_max OR
|
||||||
|
NEW.mask_s IS NOT OLD.mask_s OR
|
||||||
|
NEW.mask_l IS NOT OLD.mask_l
|
||||||
|
-- status менять разрешено, поэтому его не сравниваем
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(ABORT, "snapshots: разрешён UPDATE только поля status");
|
||||||
|
END
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- blobs: можно менять только last_seen_utc и refcount
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TRIGGER IF NOT EXISTS trg_blobs_block_update
|
||||||
|
BEFORE UPDATE ON blobs
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN
|
||||||
|
NEW.sha256 IS NOT OLD.sha256 OR
|
||||||
|
NEW.z_sha256 IS NOT OLD.z_sha256 OR
|
||||||
|
NEW.size IS NOT OLD.size OR
|
||||||
|
NEW.z_size IS NOT OLD.z_size OR
|
||||||
|
NEW.content IS NOT OLD.content OR
|
||||||
|
NEW.created_utc IS NOT OLD.created_utc OR
|
||||||
|
NEW.zstd IS NOT OLD.zstd
|
||||||
|
-- last_seen_utc и refcount менять разрешено
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(ABORT, "blobs: разрешён UPDATE только полей last_seen_utc и refcount");
|
||||||
END
|
END
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -11,9 +11,9 @@ enum SnapshotStatus : int
|
||||||
struct Snapshot
|
struct Snapshot
|
||||||
{
|
{
|
||||||
long id;
|
long id;
|
||||||
string filePath;
|
|
||||||
ubyte[32] fileSha256;
|
|
||||||
string label;
|
string label;
|
||||||
|
ubyte[32] sha256;
|
||||||
|
string description;
|
||||||
DateTime createdUtc;
|
DateTime createdUtc;
|
||||||
long sourceLength;
|
long sourceLength;
|
||||||
long algoMin;
|
long algoMin;
|
||||||
|
|
|
@ -7,10 +7,10 @@ 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.newSnapshot("/tmp/text", "Файл для тестирования", cast(ubyte[]) read("/tmp/text"));
|
cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
|
||||||
import std.stdio : writeln;
|
// import std.stdio : writeln;
|
||||||
|
|
||||||
writeln(cas.getSnapshotList("/tmp/text"));
|
writeln(cas.getSnapshotList("/tmp/text"));
|
||||||
|
|
||||||
writeln(cas.getVersion);
|
// writeln(cas.getVersion);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue