diff --git a/source/cdcdb/cdc/cas.d b/source/cdcdb/cdc/cas.d index c7f155b..bcde2af 100644 --- a/source/cdcdb/cdc/cas.d +++ b/source/cdcdb/cdc/cas.d @@ -22,54 +22,51 @@ private: DBLite _db; bool _zstd; - size_t _minSize; - size_t _normalSize; - size_t _maxSize; - size_t _maskS; - size_t _maskL; - CDC _cdc; + 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: - this( - string 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); + this(string database, bool zstd = false) + { + _db = new DBLite(database); _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) + size_t newSnapshot(string filePath, string label, const(ubyte)[] data) { - ubyte[32] sha256 = digest!SHA256(data); + ubyte[32] hashSource = digest!SHA256(data); + // Сделать запрос в БД по filePath и сверить хеш файлов - // Если последний снимок файла соответствует текущему состоянию - if (_db.isLast(label, sha256)) return 0; + if (_db.isLast(filePath, hashSource)) return 0; + + // Параметры для CDC вынести в отдельные настройки (продумать) + auto cdc = new CDC(256, 512, 1024, 0xFF, 0x0F); + // Разбить на фрагменты + auto chunks = cdc.split(data); Snapshot snapshot; + snapshot.filePath = filePath; + snapshot.fileSha256 = hashSource; 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(); @@ -92,9 +89,6 @@ public: blob.zstd = _zstd; - // Разбить на фрагменты - auto chunks = _cdc.split(data); - // Запись фрагментов в БД foreach (chunk; chunks) { @@ -139,22 +133,7 @@ public: 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), "Хеш-сумма файла не совпадает"); - + ubyte[] content = buildContent(snapshot); return content; } diff --git a/source/cdcdb/db/dblite.d b/source/cdcdb/db/dblite.d index 1c59ff4..d2bf2c2 100644 --- a/source/cdcdb/db/dblite.d +++ b/source/cdcdb/db/dblite.d @@ -7,45 +7,18 @@ import arsd.sqlite; import std.file : exists; import std.exception : enforce; import std.conv : to; -import std.string : join, replace, toLower; -import std.algorithm : canFind; -import std.format : format; +import std.string : join, replace; final class DBLite : Sqlite { private: string _dbPath; - ubyte _maxRetries; // _scheme mixin(import("scheme.d")); SqliteResult sql(T...)(string queryText, T 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); + return cast(SqliteResult) query(queryText, args); } // Проверка БД на наличие существующих в ней необходимых таблиц @@ -91,34 +64,31 @@ private: } public: - this(string database, int busyTimeout, ubyte maxRetries) + this(string database) { _dbPath = database; super(database); check(); - _maxRetries = maxRetries; - query("PRAGMA journal_mode=WAL"); query("PRAGMA synchronous=NORMAL"); query("PRAGMA foreign_keys=ON"); - query("PRAGMA busy_timeout=%d".format(busyTimeout)); } void beginImmediate() { - sql("BEGIN IMMEDIATE"); + query("BEGIN IMMEDIATE"); } void commit() { - sql("COMMIT"); + query("COMMIT"); } void rollback() { - sql("ROLLBACK"); + query("ROLLBACK"); } long addSnapshot(Snapshot snapshot) @@ -126,9 +96,9 @@ public: auto queryResult = sql( q{ INSERT INTO snapshots( + file_path, + file_sha256, label, - sha256, - description, source_length, algo_min, algo_normal, @@ -139,9 +109,9 @@ public: ) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id }, + snapshot.filePath, + snapshot.fileSha256[], snapshot.label, - snapshot.sha256[], - snapshot.description.length ? snapshot.description : null, snapshot.sourceLength, snapshot.algoMin, snapshot.algoNormal, @@ -187,17 +157,17 @@ public: ); } - bool isLast(string label, ubyte[] sha256) { + bool isLast(string filePath, ubyte[] fileSha256) { auto queryResult = sql( q{ SELECT COALESCE( - (SELECT (label = ? AND sha256 = ?) + (SELECT (file_path = ? AND file_sha256 = ?) FROM snapshots ORDER BY created_utc DESC LIMIT 1), 0 ) AS is_last; - }, label, sha256 + }, filePath, fileSha256 ); if (!queryResult.empty()) @@ -205,14 +175,14 @@ public: return false; } - Snapshot[] getSnapshots(string label) + Snapshot[] getSnapshots(string filePath) { auto queryResult = sql( q{ - SELECT id, label, sha256, description, created_utc, source_length, + 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 label = ? - }, label + FROM snapshots WHERE file_path = ? + }, filePath ); Snapshot[] snapshots; @@ -222,9 +192,9 @@ public: Snapshot snapshot; 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.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; @@ -244,7 +214,7 @@ public: { auto queryResult = sql( q{ - SELECT id, label, sha256, description, created_utc, source_length, + 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 @@ -257,9 +227,9 @@ public: 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.sha256 = cast(ubyte[]) data["sha256"].dup; - snapshot.description = data["description"].to!string; snapshot.createdUtc = toDateTime(data["created_utc"].to!string); snapshot.sourceLength = data["source_length"].to!long; snapshot.algoMin = data["algo_min"].to!long; diff --git a/source/cdcdb/db/scheme.d b/source/cdcdb/db/scheme.d index b3b10c4..c8f6176 100644 --- a/source/cdcdb/db/scheme.d +++ b/source/cdcdb/db/scheme.d @@ -6,12 +6,12 @@ auto _scheme = [ CREATE TABLE IF NOT EXISTS snapshots ( -- идентификатор снимка id INTEGER PRIMARY KEY AUTOINCREMENT, - -- метка/название снимка - label TEXT NOT NULL, + -- путь к исходному файлу + file_path TEXT, -- SHA-256 всего файла (BLOB(32)) - sha256 BLOB NOT NULL CHECK (length(sha256) = 32), - -- Комментарий/описание - description TEXT DEFAULT NULL, + file_sha256 BLOB NOT NULL CHECK (length(file_sha256) = 32), + -- метка/название снимка + label TEXT, -- время создания (UTC) created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), -- длина исходного файла в байтах @@ -88,9 +88,9 @@ auto _scheme = [ ) }, q{ - -- Индекс для запросов вида: WHERE label=? AND sha256=? + -- Индекс для запросов вида: WHERE file_path=? AND file_sha256=? CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha - ON snapshots(label, sha256) + ON snapshots(file_path, file_sha256) }, q{ -- Индекс для обратного поиска использования blob по sha256 @@ -203,51 +203,7 @@ auto _scheme = [ CREATE TRIGGER IF NOT EXISTS trg_sc_block_update BEFORE UPDATE ON snapshot_chunks BEGIN - 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"); + SELECT RAISE(ABORT, "snapshot_chunks: UPDATE запрещён; используйте DELETE + INSERT"); END } ]; diff --git a/source/cdcdb/db/types.d b/source/cdcdb/db/types.d index f1c4e53..7e64d02 100644 --- a/source/cdcdb/db/types.d +++ b/source/cdcdb/db/types.d @@ -11,9 +11,9 @@ enum SnapshotStatus : int struct Snapshot { long id; + string filePath; + ubyte[32] fileSha256; string label; - ubyte[32] sha256; - string description; DateTime createdUtc; long sourceLength; long algoMin; diff --git a/test/app.d b/test/app.d index df57add..8c360ea 100644 --- a/test/app.d +++ b/test/app.d @@ -7,10 +7,10 @@ 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; + // cas.newSnapshot("/tmp/text", "Файл для тестирования", cast(ubyte[]) read("/tmp/text")); + import std.stdio : writeln; writeln(cas.getSnapshotList("/tmp/text")); - // writeln(cas.getVersion); + writeln(cas.getVersion); }