From 1f50b21457711aeeee98ee7ca317dce4635906b3 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Tue, 30 Sep 2025 02:39:29 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=91=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D1=80=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=20=D0=91=D0=94=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=87=D0=B5=D1=81=D1=82=D0=B2=D0=B5=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B8=D1=87=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B5=D0=B9=20UUID=20=D0=B8=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=D0=BE=D0=B2.?= =?UTF-8?q?=20=D0=92=D1=80=D0=B5=D0=BC=D1=8F=20=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B8=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=20Unix=20Ti?= =?UTF-8?q?mestamp=20=D0=BC=D0=B5=D1=82=D0=BA=D0=B5=20=D1=81=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=BB=D0=BB=D0=B8=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D0=B4?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=81?= =?UTF-8?q?=D1=83=D1=89=D0=B5=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20StorageFile,?= =?UTF-8?q?=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B9=20=D1=8F=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D0=BE=D1=82=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D0=BE=D0=B9=20=D1=82=D0=BE=D1=87=D0=BA=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=D0=BE=20=D1=81=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8.=20=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=20Storage=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84=D0=BE=D0=B7=D0=BC?= =?UTF-8?q?=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=D0=BA=D0=B0=D0=BC=D0=B8.=20=D0=92=D0=B2?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D1=8B:=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=D0=BE=20=D0=B2=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=BC=20-=20UTS,=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B8=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=BC=20-=20Identifier.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- source/cdcdb/dblite.d | 956 +++++++++++++++++++++++++------------ source/cdcdb/lib/hash.d | 98 ++++ source/cdcdb/lib/package.d | 4 + source/cdcdb/lib/uts.d | 61 +++ source/cdcdb/package.d | 2 + source/cdcdb/scheme.d | 32 +- source/cdcdb/snapshot.d | 212 +------- source/cdcdb/storage.d | 180 +++---- source/cdcdb/storagefile.d | 25 + source/cdcdb/version_.d | 2 +- test/app.d | 17 +- 12 files changed, 989 insertions(+), 602 deletions(-) create mode 100644 source/cdcdb/lib/hash.d create mode 100644 source/cdcdb/lib/package.d create mode 100644 source/cdcdb/lib/uts.d create mode 100644 source/cdcdb/storagefile.d diff --git a/.gitignore b/.gitignore index 8fdd0e2..4a32f36 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ cdcdb-test-* *.obj *.lst bin -lib +/lib diff --git a/source/cdcdb/dblite.d b/source/cdcdb/dblite.d index f5a0f6b..9eb3906 100644 --- a/source/cdcdb/dblite.d +++ b/source/cdcdb/dblite.d @@ -1,32 +1,46 @@ -/// Лёгкая обёртка над SQLite с повторными попытками, схемой БД -/// и удобными структурами данных для снимков/чанков/блобов. module cdcdb.dblite; import arsd.sqlite : Sqlite, SqliteResult, DatabaseException; -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; +import std.uuid : UUID, randomUUID; + +import cdcdb.lib; + +struct DBFile { + Identifier id; + string path; + + @trusted pure nothrow @nogc @property bool empty() const { + return id.empty(); + } +} + +struct DBProcess { + Identifier id; + string name; + + @trusted pure nothrow @nogc @property bool empty() const { + return id.empty(); + } +} -/// Статус снимка. -/// - `pending` — создаётся/заполняется; -/// - `ready` — полностью подготовлен. enum SnapshotStatus : ubyte { pending = 0, ready = 1 } -/// Запись о снимке в БД (агрегированные метаданные). struct DBSnapshot { - long id; /// Идентификатор снимка. - string file; /// Имя файла (таблица `files`). + Identifier id; /// Идентификатор снимка. + DBFile file; /// Файл (таблица `files`). ubyte[32] sha256; /// Хеш всего файла (SHA-256, 32 байта). string description; /// Описание/комментарий (может быть пустым). - DateTime createdUtc; /// Время создания (UTC). + UTS createdUtc; /// Время создания (UTC). long sourceLength; /// Длина исходного файла (байт). long algoMin; /// FastCDC: минимальный размер чанка. long algoNormal; /// FastCDC: нормальный (целевой) размер чанка. @@ -38,19 +52,21 @@ struct DBSnapshot { long ruid; /// Real UID процесса. string uidName; /// Имя пользователя для `uid`. string ruidName; /// Имя пользователя для `ruid`. - string process; /// Имя процесса (таблица `processes`). + DBProcess process; /// Процесс (таблица `processes`). + + @trusted pure nothrow @nogc @property bool empty() const { + return id.empty(); + } } -/// Связь снимка с чанками (индексы и хеши). struct DBSnapshotChunk { - long snapshotId; /// ID снимка. - long chunkIndex; /// Порядковый номер чанка в снимке. - long offset; /// Смещение чанка в файле. - ubyte[32] sha256; /// Хеш чанка (SHA-256, 32 байта). + Identifier snapshotId; /// ID снимка. + long chunkIndex; /// Порядковый номер чанка в снимке. + long offset; /// Смещение чанка в файле. + ubyte[32] sha256; /// Хеш чанка (SHA-256, 32 байта). } -/// Запись о блобе (уникальный чанк) в БД. struct DBBlob { ubyte[32] sha256; /// Хеш исходного содержимого. @@ -58,14 +74,12 @@ struct DBBlob long size; /// Размер исходного содержимого. long zSize; /// Размер сжатого содержимого. ubyte[] content; /// Контент (если хранится в БД). - DateTime createdUtc; /// Время создания (UTC). - DateTime lastSeenUtc; /// Последний доступ (UTC). + UTS createdUtc; /// Время создания (UTC). + UTS lastSeenUtc; /// Последний доступ (UTC). long refcount; /// Ссылки на блоб (сколькими снимками используется). bool zstd; /// Признак, что `content` хранится в сжатом виде. } -/// Расширенная выборка чанков для восстановления. -/// Содержит и метаданные, и (возможное) содержимое. struct DBSnapshotChunkData { long chunkIndex; /// Порядковый номер чанка. long offset; /// Смещение в файле. @@ -77,10 +91,6 @@ struct DBSnapshotChunkData { ubyte[32] zSha256; /// Хеш сжатого содержимого. } -/// Простейший клиент SQLite с: -/// - автоматической инициализацией схемы (при пустой БД); -/// - повторными попытками при блокировках; -/// - удобными методами для CRUD по объектам домена. final class DBLite : Sqlite { private: @@ -157,21 +167,7 @@ private: } } - /// Переводит текстовую дату из SQLite (`YYYY-MM-DD HH:MM:SS.SSS`) - /// в `DateTime` (ISO 8601 с `T`). - DateTime toDateTime(string sqliteDate) - { - string isoDate = sqliteDate.replace(" ", "T"); - return DateTime.fromISOExtString(isoDate); - } - public: - /// Открывает БД, проверяет/инициализирует схему и настраивает PRAGMA. - /// - /// Параметры: - /// - `database` — путь к файлу БД; - /// - `busyTimeout` — таймаут ожидания блокировок (мс); - /// - `maxRetries` — число повторов при `busy/locked`. this(string database, size_t busyTimeout, size_t maxRetries) { _dbPath = database; @@ -205,24 +201,179 @@ public: sql("ROLLBACK"); } - /// Проверяет, совпадает ли последний снимок для `file` с заданным `sha256`. - /// - /// Возвращает `true`, если самый свежий снимок этого файла имеет тот же SHA-256. - bool isLast(string file, ubyte[] sha256) { + /************************************************* + **************** Работа с файлом ***************** + *************************************************/ + + DBFile[] getFiles() { + auto queryResult = sql( + q{ + SELECT id, name FROM files + } + ); + + DBFile[] files; + + foreach (row; queryResult) + { + DBFile file; + + file.id = cast(ubyte[]) row["id"].dup; + file.path = row["name"].to!string; + + files ~= file; + } + + return files; + } + + DBFile getFile(string path) { + auto queryResult = sql( + q{ + SELECT id FROM files WHERE name = ?1 + }, path + ); + + DBFile file; + + if (!queryResult.empty()) { + auto data = queryResult.front(); + + file.id = cast(ubyte[]) data["id"].dup; + file.path = path; + } + + return file; + } + + DBFile getFile(Identifier id) { + auto queryResult = sql( + q{ + SELECT id, name + FROM files + WHERE id = ?1 + }, id[] + ); + + DBFile file; + + if (!queryResult.empty()) { + auto data = queryResult.front(); + + file.id = id; + file.path = data["name"].to!string; + } + + return file; + } + + DBFile addFile(string path) { + SqliteResult queryResult; + UUID uuid; + + // Исключение одинакового UUID первичного ключа + do { + uuid = randomUUID(); + queryResult = sql( + q{ + SELECT id FROM files WHERE id = ?1 + }, uuid.data[] + ); + } while (!queryResult.empty()); + + queryResult = sql( + q{ + INSERT INTO files(id, name) + VALUES(?1, ?2) + RETURNING id, name + }, uuid.data[], path + ); + + enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); + + return DBFile(Identifier(uuid.data), path); + } + + DBFile[] findFile(string pattern) { + auto queryResult = sql( + q{ + SELECT id, name FROM files WHERE name LIKE ?1 + }, '%' ~ pattern ~ '%' + ); + + DBFile[] files; + + foreach (row; queryResult) + { + DBFile file; + + file.id = cast(ubyte[]) row["id"].dup; + file.path = row["name"].to!string; + + files ~= file; + } + + return files; + } + + // Функция производит поиск по хешу + // Если младший ниббл является нулевым (0000), то ищем позицию вхождения подстроки в строку + // Иначе ищем по подстроке + DBFile[] findFile(Identifier id) { + auto queryResult = sql( + q{ + SELECT id, name + FROM files + WHERE length(?1) BETWEEN 1 AND 16 + AND CASE + WHEN substr(hex(?1), 2 * length(?1), 1) = '0' + THEN instr(hex(id), substr(hex(?1), 1, 2 * length(?1) - 1)) > 0 + ELSE substr(id, 1, length(?1)) = ?1 + END + }, id[] + ); + + DBFile[] files; + + foreach (row; queryResult) + { + DBFile file; + + file.id = cast(ubyte[]) row["id"].dup; + file.path = row["name"].to!string; + + files ~= file; + } + + return files; + } + + bool deleteFile(Identifier id) { + auto queryResult = sql("DELETE FROM files WHERE id = ?1 RETURNING id", id[]); + return !queryResult.empty(); + } + + bool deleteFile(string path) { + auto queryResult = sql("DELETE FROM files WHERE name = ?1 RETURNING id", path); + return !queryResult.empty(); + } + + ///////////////////////////////////////////////////////////////////// + + bool isLast(Identifier id, ubyte[] sha256) { auto queryResult = sql( q{ SELECT COALESCE( ( - SELECT (s.sha256 = ?2) - FROM snapshots s - JOIN files f ON f.id = s.file - WHERE f.name = ?1 - ORDER BY s.created_utc DESC + SELECT (sha256 = ?2) + FROM snapshots + WHERE file = ?1 + ORDER BY created_utc DESC LIMIT 1 ), 0 ) AS is_last; - }, file, sha256 + }, id[], sha256[] ); if (!queryResult.empty()) @@ -230,111 +381,6 @@ public: return false; } - /// Добавляет новый снимок. Возвращает его `id`. - long addSnapshot(DBSnapshot snapshot) - { - auto queryResult = sql( - q{ - INSERT INTO snapshots( - file, - sha256, - description, - source_length, - uid, - ruid, - process, - algo_min, - algo_normal, - algo_max, - mask_s, - mask_l, - status - ) - SELECT - (SELECT id FROM files WHERE name = ?), - ?,?,?,?,?, - (SELECT id FROM processes WHERE name = ?), - ?,?,?,?,?,? - RETURNING id - }, - snapshot.file, - snapshot.sha256[], - snapshot.description.length ? snapshot.description : null, - snapshot.sourceLength, - snapshot.uid, - snapshot.ruid, - snapshot.process, - snapshot.algoMin, - snapshot.algoNormal, - snapshot.algoMax, - snapshot.maskS, - snapshot.maskL, - snapshot.status.to!int - ); - - if (queryResult.empty()) { - throw new Exception("Ошибка добавления нового снимка в базу данных"); - } - - return queryResult.front()["id"].to!long; - } - - /// Добавляет блоб. Возвращает `true`, если вставка произошла (не было конфликта). - bool addBlob(DBBlob blob) - { - auto queryResult = sql( - q{ - INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd) - SELECT ?1,?2,?3,?4,?5,?6 - WHERE NOT EXISTS ( - SELECT 1 FROM blobs WHERE sha256 = ?1 - ) - RETURNING sha256 - }, - blob.sha256[], - blob.zstd ? blob.zSha256[] : null, - blob.size, - blob.zSize, - blob.content, - blob.zstd.to!int - ); - - return !queryResult.empty(); - } - - /// Добавляет процесс по имени (идемпотентно). - bool addProcess(string name) - { - auto queryResult = sql( - q{ - INSERT INTO processes (name) - SELECT ?1 - WHERE NOT EXISTS ( - SELECT 1 FROM processes WHERE name = ?1 - ) - }, name - ); - - return !queryResult.empty(); - } - - /// Добавляет файл по имени (идемпотентно). - bool addFile(string name) - { - auto queryResult = sql( - q{ - INSERT INTO files (name) - SELECT ?1 - WHERE NOT EXISTS ( - SELECT 1 FROM files WHERE name = ?1 - ) - }, name - ); - - return !queryResult.empty(); - } - - /// Добавляет пользователя (uid, name) (идемпотентно). bool addUser(long uid, string name) { auto queryResult = sql( @@ -350,16 +396,186 @@ public: return !queryResult.empty(); } - /// Добавляет связь снимок–чанк. + DBProcess getProcess(string name) { + auto queryResult = sql( + q{ + SELECT id FROM processes WHERE name = ?1 + }, name + ); + + DBProcess process; + + if (!queryResult.empty()) { + auto data = queryResult.front(); + + process.id = cast(ubyte[]) data["id"].dup; + process.name = name; + } + + return process; + } + + DBProcess getProcess(Identifier id) { + auto queryResult = sql( + q{ + SELECT id, name + FROM processes + WHERE id = ?1 + }, id[] + ); + + DBProcess process; + + if (!queryResult.empty()) { + auto data = queryResult.front(); + + process.id = id; + process.name = data["name"].to!string; + } + + return process; + } + + DBProcess addProcess(string name) { + SqliteResult queryResult; + UUID uuid; + + // Исключение одинакового UUID первичного ключа + do { + uuid = randomUUID(); + queryResult = sql( + q{ + SELECT id FROM processes WHERE id = ?1 + }, uuid.data[] + ); + } while (!queryResult.empty()); + + queryResult = sql( + q{ + INSERT INTO processes (id, name) + VALUES (?1, ?2) + RETURNING id, name + }, uuid.data[], name + ); + + enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); + + return DBProcess(Identifier(uuid.data), name); + } + + ///////////////////////////////////////////////////////////////////// + + bool addSnapshot(ref DBSnapshot snapshot) + { + SqliteResult queryResult; + UUID uuid; + + // Исключение одинакового UUID первичного ключа + do { + uuid = randomUUID(); + queryResult = sql( + q{ + SELECT id FROM snapshots WHERE id = ?1 + }, uuid.data[] + ); + } while (!queryResult.empty()); + + import std.datetime : Clock; + + snapshot.createdUtc = UTS(Clock.currTime()); + snapshot.id = Identifier(uuid.data); + + queryResult = sql( + q{ + INSERT INTO snapshots( + id, + file, + sha256, + description, + created_utc, + source_length, + uid, + ruid, + process, + algo_min, + algo_normal, + algo_max, + mask_s, + mask_l, + status + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15) + RETURNING id + }, + snapshot.id[], // ?1 + snapshot.file.id[], // ?2 + snapshot.sha256[], // ?3 + snapshot.description.length ? snapshot.description : null, // ?4 + snapshot.createdUtc.unix, // ?5 + snapshot.sourceLength, // ?6 + snapshot.uid, // ?7 + snapshot.ruid, // ?8 + snapshot.process.id[], // ?9 + snapshot.algoMin, // ?10 + snapshot.algoNormal, // ?11 + snapshot.algoMax, // ?12 + snapshot.maskS, // ?13 + snapshot.maskL, // ?14 + snapshot.status.to!int // ?15 + ); + + return !queryResult.empty(); + } + + ///////////////////////////////////////////////////////////////////// + + bool addBlob(ref DBBlob blob) + { + import std.datetime : Clock; + + blob.createdUtc = UTS(Clock.currTime()); + blob.lastSeenUtc = UTS(Clock.currTime()); + + auto queryResult = sql( + q{ + INSERT INTO blobs ( + sha256, + z_sha256, + size, + z_size, + content, + created_utc, + last_seen_utc, + zstd + ) + SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8 + WHERE NOT EXISTS ( + SELECT 1 FROM blobs WHERE sha256 = ?1 + ) + RETURNING sha256 + }, + blob.sha256[], // ?1 + blob.zstd ? blob.zSha256[] : null, // ?2 + blob.size, // ?3 + blob.zSize, // ?4 + blob.content, // ?5 + blob.createdUtc.unix, // ?6 + blob.lastSeenUtc.unix, // ?7 + blob.zstd.to!int // ?8 + ); + + return !queryResult.empty(); + } + bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) { auto queryResult = sql( q{ INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256) - VALUES(?,?,?,?) + VALUES (?1, ?2, ?3, ?4) RETURNING snapshot_id }, - snapshotChunk.snapshotId, + snapshotChunk.snapshotId[], snapshotChunk.chunkIndex, snapshotChunk.offset, snapshotChunk.sha256[] @@ -368,130 +584,7 @@ public: return !queryResult.empty(); } - /// Возвращает один снимок по `id`. Если не найден — вернёт пустую структуру. - DBSnapshot getSnapshot(long id) - { - auto queryResult = sql( - q{ - SELECT - s.id, - f.name file, - s.sha256, - s.description, - s.created_utc, - s.source_length, - s.uid, - s.ruid, - u.name uid_name, - r.name ruid_name, - p.name process, - s.algo_min, - s.algo_normal, - s.algo_max, - s.mask_s, - s.mask_l, - s.status - FROM snapshots s - JOIN processes p ON p.id = s.process - JOIN users u ON u.uid = s.uid - JOIN users r ON r.uid = s.ruid - JOIN files f ON f.id = s.file - WHERE s.id = ?; - }, id - ); - - DBSnapshot snapshot; - - if (!queryResult.empty()) - { - auto data = queryResult.front(); - - snapshot.id = data["id"].to!long; - snapshot.file = data["file"].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; - 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; - snapshot.uid = data["uid"].to!long; - snapshot.ruid = data["ruid"].to!long; - snapshot.uidName = data["uid_name"].to!string; - snapshot.ruidName = data["ruid_name"].to!string; - snapshot.process = data["process"].to!string; - } - - return snapshot; - } - - /// Возвращает список снимков (опционально фильтр по имени файла). - DBSnapshot[] getSnapshots(string file) - { - auto queryResult = sql( - q{ - SELECT - s.id, - f.name file, - s.sha256, - s.description, - s.created_utc, - s.source_length, - s.uid, - s.ruid, - u.name uid_name, - r.name ruid_name, - p.name process, - s.algo_min, - s.algo_normal, - s.algo_max, - s.mask_s, - s.mask_l, - s.status - FROM snapshots s - JOIN processes p ON p.id = s.process - JOIN users u ON u.uid = s.uid - JOIN users r ON r.uid = s.ruid - JOIN files f ON f.id = s.file AND (length(?) = 0 OR f.name = ?1) - ORDER BY s.created_utc, s.id; - }, file - ); - - DBSnapshot[] snapshots; - - foreach (row; queryResult) - { - DBSnapshot snapshot; - - snapshot.id = row["id"].to!long; - snapshot.file = row["file"].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; - snapshot.uid = row["uid"].to!long; - snapshot.ruid = row["ruid"].to!long; - snapshot.uidName = row["uid_name"].to!string; - snapshot.ruidName = row["ruid_name"].to!string; - snapshot.process = row["process"].to!string; - - snapshots ~= snapshot; - } - - return snapshots; - } - - /// Возвращает последовательность чанков снимка c данными. - DBSnapshotChunkData[] getChunks(long snapshotId) + DBSnapshotChunkData[] getChunks(Identifier id) { auto queryResult = sql( q{ @@ -501,7 +594,7 @@ public: JOIN blobs b ON b.sha256 = sc.sha256 WHERE sc.snapshot_id = ? ORDER BY sc.chunk_index - }, snapshotId + }, id[] ); DBSnapshotChunkData[] sdchs; @@ -525,29 +618,290 @@ public: return sdchs; } - /// Удаляет один снимок по `id`. Возвращает `id` удалённой строки или 0. - long deleteSnapshot(long id) { - auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id); - - if (!queryResult.empty()) { - return queryResult.front()["id"].to!long; - } - return 0; - } - - /// Удаляет все снимки по имени файла. Возвращает число удалённых строк. - long deleteSnapshots(string file) { + DBSnapshot[] getSnapshots(string file) + { auto queryResult = sql( q{ - DELETE FROM snapshots - WHERE file = (SELECT id FROM files WHERE name = ?) - RETURNING 1; - }, file); + SELECT + s.id, + f.id file_id, + f.name file_name, + s.sha256, + s.description, + s.created_utc, + s.source_length, + s.uid, + s.ruid, + u.name uid_name, + r.name ruid_name, + p.id process_id, + p.name process_name, + s.algo_min, + s.algo_normal, + s.algo_max, + s.mask_s, + s.mask_l, + s.status + FROM snapshots s + JOIN processes p ON p.id = s.process + JOIN users u ON u.uid = s.uid + JOIN users r ON r.uid = s.ruid + JOIN files f ON f.id = s.file AND (length(?) = 0 OR f.name = ?1) + ORDER BY s.created_utc + }, file + ); - if (!queryResult.empty()) { - return queryResult.length.to!long; + DBSnapshot[] snapshots; + + foreach (row; queryResult) + { + DBSnapshot snapshot; + + snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.file = DBFile( + Identifier(cast(ubyte[]) row["file_id"].dup), + row["file_name"].to!string + ); + snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; + snapshot.description = row["description"].to!string; + snapshot.createdUtc = row["created_utc"].to!long; + 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; + snapshot.uid = row["uid"].to!long; + snapshot.ruid = row["ruid"].to!long; + snapshot.uidName = row["uid_name"].to!string; + snapshot.ruidName = row["ruid_name"].to!string; + snapshot.process = DBProcess( + Identifier(cast(ubyte[]) row["process_id"].dup), + row["process_name"].to!string + ); + + snapshots ~= snapshot; } - return 0; + return snapshots; + } + + DBSnapshot[] getSnapshots(Identifier id) + { + auto queryResult = sql( + q{ + SELECT + s.id, + f.id file_id, + f.name file_name, + s.sha256, + s.description, + s.created_utc, + s.source_length, + s.uid, + s.ruid, + u.name uid_name, + r.name ruid_name, + p.id process_id, + p.name process_name, + s.algo_min, + s.algo_normal, + s.algo_max, + s.mask_s, + s.mask_l, + s.status + FROM snapshots s + JOIN processes p ON p.id = s.process + JOIN users u ON u.uid = s.uid + JOIN users r ON r.uid = s.ruid + JOIN files f ON f.id = s.file + WHERE s.id = ?1 + ORDER BY s.created_utc + }, id[] + ); + + DBSnapshot[] snapshots; + + foreach (row; queryResult) + { + DBSnapshot snapshot; + + snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.file = DBFile( + Identifier(cast(ubyte[]) row["file_id"].dup), + row["file_name"].to!string + ); + snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; + snapshot.description = row["description"].to!string; + snapshot.createdUtc = row["created_utc"].to!long; + 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; + snapshot.uid = row["uid"].to!long; + snapshot.ruid = row["ruid"].to!long; + snapshot.uidName = row["uid_name"].to!string; + snapshot.ruidName = row["ruid_name"].to!string; + snapshot.process = DBProcess( + Identifier(cast(ubyte[]) row["process_id"].dup), + row["process_name"].to!string + ); + + snapshots ~= snapshot; + } + + return snapshots; + } + + DBSnapshot getSnapshot(Identifier id) + { + auto queryResult = sql( + q{ + SELECT + s.id, + f.id file_id, + f.name file_name, + s.sha256, + s.description, + s.created_utc, + s.source_length, + s.uid, + s.ruid, + u.name uid_name, + r.name ruid_name, + p.id process_id, + p.name process_name, + s.algo_min, + s.algo_normal, + s.algo_max, + s.mask_s, + s.mask_l, + s.status + FROM snapshots s + JOIN processes p ON p.id = s.process + JOIN users u ON u.uid = s.uid + JOIN users r ON r.uid = s.ruid + JOIN files f ON f.id = s.file + WHERE f.id = ?1 + ORDER BY s.created_utc + }, id[] + ); + + DBSnapshot snapshot; + + if (!queryResult.empty()) { + auto data = queryResult.front(); + + snapshot.id = cast(ubyte[]) data["id"].dup; + snapshot.file = DBFile( + Identifier(cast(ubyte[]) data["file_id"].dup), + data["file_name"].to!string + ); + snapshot.sha256 = cast(ubyte[]) data["sha256"].dup; + snapshot.description = data["description"].to!string; + snapshot.createdUtc = data["created_utc"].to!long; + 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; + snapshot.uid = data["uid"].to!long; + snapshot.ruid = data["ruid"].to!long; + snapshot.uidName = data["uid_name"].to!string; + snapshot.ruidName = data["ruid_name"].to!string; + snapshot.process = DBProcess( + Identifier(cast(ubyte[]) data["process_id"].dup), + data["process_name"].to!string + ); + } + + return snapshot; + } + + // Функция производит поиск по хешу + // Если младший ниббл является нулевым (0000), то ищем позицию вхождения подстроки в строку + // Иначе ищем по подстроке + DBSnapshot[] findSnapshot(Identifier id) { + auto queryResult = sql( + q{ + SELECT + s.id, + f.id file_id, + f.name file_name, + s.sha256, + s.description, + s.created_utc, + s.source_length, + s.uid, + s.ruid, + u.name uid_name, + r.name ruid_name, + p.id process_id, + p.name process_name, + s.algo_min, + s.algo_normal, + s.algo_max, + s.mask_s, + s.mask_l, + s.status + FROM snapshots s + JOIN processes p ON p.id = s.process + JOIN users u ON u.uid = s.uid + JOIN users r ON r.uid = s.ruid + JOIN files f ON f.id = s.file + WHERE length(?1) BETWEEN 1 AND 16 + AND CASE + WHEN substr(hex(?1), 2 * length(?1), 1) = '0' + THEN instr(hex(s.id), substr(hex(?1), 1, 2 * length(?1) - 1)) > 0 + ELSE substr(s.id, 1, length(?1)) = ?1 + END + }, id[] + ); + + DBSnapshot[] snapshots; + + foreach (row; queryResult) + { + DBSnapshot snapshot; + + snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.file = DBFile( + Identifier(cast(ubyte[]) row["file_id"].dup), + row["file_name"].to!string + ); + snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; + snapshot.description = row["description"].to!string; + snapshot.createdUtc = row["created_utc"].to!long; + 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; + snapshot.uid = row["uid"].to!long; + snapshot.ruid = row["ruid"].to!long; + snapshot.uidName = row["uid_name"].to!string; + snapshot.ruidName = row["ruid_name"].to!string; + snapshot.process = DBProcess( + Identifier(cast(ubyte[]) row["process_id"].dup), + row["process_name"].to!string + ); + + snapshots ~= snapshot; + } + + return snapshots; + } + + bool deleteSnapshot(Identifier id) { + auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id[]); + return !queryResult.empty(); } } diff --git a/source/cdcdb/lib/hash.d b/source/cdcdb/lib/hash.d new file mode 100644 index 0000000..4303c71 --- /dev/null +++ b/source/cdcdb/lib/hash.d @@ -0,0 +1,98 @@ +module cdcdb.lib.hash; + +import std.format : format; + +struct Identifier +{ +private: + ubyte[] _data; + + ubyte hxc(ref const char c) const + { + auto lc = cast(char)(c | 32); + if (lc >= '0' && lc <= '9') + return cast(ubyte)(lc - '0'); + if (lc >= 'a' && lc <= 'f') + return cast(ubyte)(10 + lc - 'a'); + throw new Exception("Некорректный символ hex"); + } + + ubyte[] fromHex(ref const string hash) const + { + import std.exception : enforce; + + enforce(hash.length > 0, "Hex-строка не может быть пустой."); + enforce(hash.length <= 32, "Длина hex-строки не должна превышать 32 символа."); + + size_t byteLen = (hash.length + 1) / 2; // Округление вверх для нечётной длины + ubyte[] data = new ubyte[byteLen]; + + foreach (i; 0 .. hash.length / 2) + { + data[i] = cast(ubyte)((hxc(hash[2 * i]) << 4) | hxc(hash[2 * i + 1])); + } + + if (hash.length % 2 != 0) + { + // Для нечётной длины: последний ниббл в старший разряд, младший = 0 + data[$ - 1] = cast(ubyte)(hxc(hash[$ - 1]) << 4); + } + + return data; + } + +public: + // alias _data this; + + this(const string hex) + { + _data = fromHex(hex); + } + + void opAssign(const string hex) + { + _data = fromHex(hex); + } + + this(ubyte[] data) + { + assert(data.length <= 16); + _data = data; + } + + this(ref const ubyte[16] data) + { + assert(data.length <= 16); + _data = data.dup; + } + + void opAssign(ubyte[] data) + { + assert(data.length <= 16); + _data = data; + } + + string toString() const @safe pure + { + return format("%(%02x%)", _data); + } + + string compact(int size = 4) const @safe pure + { + auto length = _data.length >= size && size > 0 ? size : _data.length; + return format("%(%02x%)", _data[0 .. length]); + } + + ubyte[] data() + { + return _data; + } + + ubyte[] opIndex() { + return _data; + } + + @trusted pure nothrow @nogc @property bool empty() const { + return _data.length == 0; + } +} diff --git a/source/cdcdb/lib/package.d b/source/cdcdb/lib/package.d new file mode 100644 index 0000000..d5aaabf --- /dev/null +++ b/source/cdcdb/lib/package.d @@ -0,0 +1,4 @@ +module cdcdb.lib; + +public import cdcdb.lib.hash; +public import cdcdb.lib.uts; diff --git a/source/cdcdb/lib/uts.d b/source/cdcdb/lib/uts.d new file mode 100644 index 0000000..f699aa6 --- /dev/null +++ b/source/cdcdb/lib/uts.d @@ -0,0 +1,61 @@ +module cdcdb.lib.uts; + +import std.datetime : SysTime, msecs; + +// 2050-01-01 00:00:00 UTC +private enum UTS_LAST_TS = 0x967a7600; // 2524608000L +// Extended +private enum UTS_LAST_TS_EXT = UTS_LAST_TS * 1_000L; + +// Unix Timestamp с миллисекундами +struct UTS +{ +private: + long _ts; + + long calc(SysTime systime) { + long millis = systime.toUnixTime() * 1000L + systime.fracSecs.total!"msecs"; + return millis; + } + +public: + this(long ts) { + assert(ts < UTS_LAST_TS_EXT); + _ts = ts; + } + + this(SysTime systime) { + _ts = calc(systime); + } + + void opAssign(SysTime systime) { + _ts = calc(systime); + } + + void opAssign(long ts) { + assert(ts < UTS_LAST_TS_EXT); + _ts = ts; + } + + string toString() const + { + import std.format : format; + + string formatStr = "%04d-%02d-%02d %02d:%02d:%02d.%03d"; + long seconds = _ts / 1_000L; + long millis = _ts % 1_000L; + auto sysTime = SysTime.fromUnixTime(seconds) + msecs(millis); + return format(formatStr, + sysTime.year, sysTime.month, sysTime.day, + sysTime.hour, sysTime.minute, sysTime.second, + sysTime.fracSecs.total!"msecs"); + } + + @property const(SysTime) sys() const @safe { + return SysTime.fromUnixTime(_ts / 1_000L); + } + + @property long unix() const @safe { + return _ts; + } +} diff --git a/source/cdcdb/package.d b/source/cdcdb/package.d index 167d305..72dc0e6 100644 --- a/source/cdcdb/package.d +++ b/source/cdcdb/package.d @@ -1,4 +1,6 @@ module cdcdb; +public import cdcdb.lib; public import cdcdb.storage; +public import cdcdb.storagefile; public import cdcdb.snapshot; diff --git a/source/cdcdb/scheme.d b/source/cdcdb/scheme.d index 40ab4db..0a70b42 100644 --- a/source/cdcdb/scheme.d +++ b/source/cdcdb/scheme.d @@ -21,10 +21,10 @@ auto _scheme = [ -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS processes ( -- идентификатор процесса - id INTEGER PRIMARY KEY AUTOINCREMENT, + id BLOB PRIMARY KEY, -- имя процесса name TEXT NOT NULL UNIQUE - ) + ) WITHOUT ROWID }, q{ -- Индекс по имени процесса @@ -37,10 +37,10 @@ auto _scheme = [ -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS files ( -- идентификатор файла - id INTEGER PRIMARY KEY AUTOINCREMENT, + id BLOB PRIMARY KEY, -- имя файла name TEXT NOT NULL UNIQUE - ) + ) WITHOUT ROWID }, q{ -- Индекс по имени файла @@ -53,15 +53,15 @@ auto _scheme = [ -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS snapshots ( -- идентификатор снимка - id INTEGER PRIMARY KEY AUTOINCREMENT, + id BLOB PRIMARY KEY, -- Файл - file INTEGER NOT NULL, + file BLOB NOT NULL, -- SHA-256 всего файла (BLOB(32)) sha256 BLOB NOT NULL CHECK (length(sha256) = 32), -- Комментарий/описание description TEXT DEFAULT NULL, -- время создания (UTC) - created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + created_utc INTEGER NOT NULL, -- длина исходного файла в байтах source_length INTEGER NOT NULL, -- UID пользователя (эффективный) @@ -69,7 +69,7 @@ auto _scheme = [ -- RUID пользователя (реальный) ruid INTEGER NOT NULL, -- Процесс - process INTEGER NOT NULL, + process BLOB NOT NULL, -- FastCDC: минимальный размер чанка algo_min INTEGER NOT NULL, -- FastCDC: целевой размер чанка @@ -100,7 +100,7 @@ auto _scheme = [ REFERENCES files(id) ON UPDATE CASCADE ON DELETE CASCADE - ) + ) WITHOUT ROWID }, q{ -- ------------------------------------------------------------ @@ -118,9 +118,9 @@ auto _scheme = [ -- байты (сжатые при zstd=1, иначе исходные) content BLOB NOT NULL, -- время создания записи (UTC) - created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + created_utc INTEGER NOT NULL, -- время последней ссылки (UTC) - last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), + last_seen_utc INTEGER NOT NULL, -- число ссылок из snapshot_chunks refcount INTEGER NOT NULL DEFAULT 0, -- 0=нет сжатия, 1=zstd @@ -132,7 +132,7 @@ auto _scheme = [ (zstd = 0 AND length(content) = size) ), CHECK (z_sha256 IS NULL OR length(z_sha256) = 32) - ) + ) WITHOUT ROWID }, q{ -- ------------------------------------------------------------ @@ -140,7 +140,7 @@ auto _scheme = [ -- ------------------------------------------------------------ CREATE TABLE IF NOT EXISTS snapshot_chunks ( -- FK -> snapshots.id - snapshot_id INTEGER NOT NULL, + snapshot_id BLOB NOT NULL, -- порядковый номер чанка в снимке chunk_index INTEGER NOT NULL, -- смещение чанка в исходном файле, байт @@ -156,7 +156,7 @@ auto _scheme = [ REFERENCES blobs(sha256) ON UPDATE RESTRICT ON DELETE RESTRICT - ) + ) WITHOUT ROWID }, q{ -- Индекс для запросов вида: WHERE file=? AND sha256=? @@ -178,7 +178,7 @@ auto _scheme = [ BEGIN UPDATE blobs SET refcount = refcount + 1, - last_seen_utc = CURRENT_TIMESTAMP + last_seen_utc = cast(unixepoch("subsecond") * 1000 as int) WHERE sha256 = NEW.sha256; END }, @@ -213,7 +213,7 @@ auto _scheme = [ UPDATE blobs SET refcount = refcount + 1, - last_seen_utc = CURRENT_TIMESTAMP + last_seen_utc = cast(unixepoch("subsecond") * 1000 as int) WHERE sha256 = NEW.sha256; END }, diff --git a/source/cdcdb/snapshot.d b/source/cdcdb/snapshot.d index 88e4541..8cd534e 100644 --- a/source/cdcdb/snapshot.d +++ b/source/cdcdb/snapshot.d @@ -1,42 +1,21 @@ module cdcdb.snapshot; import cdcdb.dblite; +import cdcdb.lib; import zstd : uncompress; import std.digest.sha : SHA256, digest; -import std.datetime : DateTime; import std.exception : enforce; +import std.datetime : SysTime; -/** - * Чтение снимка и управление его жизненным циклом. - * - * Класс собирает полный файл из чанков, хранящихся через `DBLite`, - * проверяет целостность (SHA-256 каждого чанка и итогового файла) - * и предоставляет безопасное удаление записи о снимке. - * - * Пример: - * --- - * auto s1 = new Snapshot(db, snapshotId); - * auto bytes = s1.data(); // материализовать весь контент в память - * - * // или потоково, без крупной аллокации: - * s1.data((const(ubyte)[] part) { - * // обработать part - * }); - * --- - * - * Заметки: - * - Все проверки целостности обязательны; любое расхождение вызывает исключение. - * - Для очень больших файлов предпочтительнее потоковый вариант с делегатом. - */ final class Snapshot { private: DBLite _db; DBSnapshot _snapshot; - /// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша. + // Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша. const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk) { ubyte[] bytes; @@ -57,36 +36,8 @@ private: } public: - /// Создать `Snapshot` из уже загруженной строки `DBSnapshot`. - /// - /// Параметры: - /// dblite = хэндл базы данных - /// dbSnapshot = метаданные снимка, полученные ранее - this(DBLite dblite, DBSnapshot dbSnapshot) - { - _db = dblite; - _snapshot = dbSnapshot; - } + this(DBLite dblite, DBSnapshot dbSnapshot) { _db = dblite; _snapshot = dbSnapshot; } - /// Создать `Snapshot`, подгрузив метаданные из базы. - /// - /// Параметры: - /// dblite = хэндл базы данных - /// idSnapshot = идентификатор снимка - this(DBLite dblite, long idSnapshot) - { - _db = dblite; - _snapshot = _db.getSnapshot(idSnapshot); - } - - /// Материализует полный контент файла в память. - /// - /// Собирает чанки по порядку, проверяет SHA-256 каждого чанка и - /// итоговый SHA-256 файла (`snapshots.sha256`). - /// - /// Возвращает: новый буфер `ubyte[]` с полным содержимым. - /// - /// Бросает: Exception при любой ошибке целостности. ubyte[] data() { auto chunks = _db.getChunks(_snapshot.id); @@ -107,15 +58,6 @@ public: return content; } - /// Потоково передаёт содержимое файла в заданный приёмник. - /// - /// Избегает одной большой аллокации: чанк декодируется, проверяется - /// и передаётся в `sink` по порядку. - /// - /// Параметры: - /// sink = делегат, вызываемый для каждого проверенного чанка. - /// - /// Бросает: Exception при любой ошибке целостности. void data(void delegate(const(ubyte)[]) sink) { auto chunks = _db.getChunks(_snapshot.id); @@ -131,154 +73,46 @@ public: enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает"); } - /// Удаляет снимок из базы в транзакции. - /// - /// Открывает транзакцию IMMEDIATE, удаляет запись о снимке и коммитит. - /// В случае ошибки откатывает. - /// - /// Возвращает: `true`, если запись была удалена. - /// - /// Примечание: не выполняет сборку мусора по блобам. - 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; - } - // ----------------------------- // Доступ к метаданным снимка // ----------------------------- /// ID снимка (PRIMARY KEY). - @property long id() const nothrow @safe - { - return _snapshot.id; - } - + @property Identifier id() nothrow @safe { return _snapshot.id; } /// Имя файла (из таблицы `files`). - @property string file() const nothrow @safe - { - return _snapshot.file; - } - + @property string file() const nothrow @safe { return _snapshot.file.path; } /// Время создания (UTC). - @property DateTime created() const @safe - { - return _snapshot.createdUtc; - } - + @property const(SysTime) created() const @safe { return _snapshot.createdUtc.sys; } /// Длина исходного файла (байты). - @property long length() const nothrow @safe - { - return _snapshot.sourceLength; - } - + @property long length() const nothrow @safe { return _snapshot.sourceLength; } /// Ожидаемый SHA-256 всего файла (сырые 32 байта). - @property ubyte[32] sha256() const nothrow @safe - { - return _snapshot.sha256; - } - + @property ubyte[32] sha256() const nothrow @safe { return _snapshot.sha256; } /// Статус снимка (строкой). - @property string status() const - { - import std.conv : to; - - return _snapshot.status.to!string; - } - + @property string status() const { import std.conv : to; return _snapshot.status.to!string; } /// Необязательное описание. - @property string description() const nothrow @safe - { - return _snapshot.description; - } - + @property string description() const nothrow @safe { return _snapshot.description; } /// FastCDC: минимальный размер чанка. - @property long algoMin() const nothrow @safe - { - return _snapshot.algoMin; - } - + @property long algoMin() const nothrow @safe { return _snapshot.algoMin; } /// FastCDC: целевой (нормальный) размер чанка. - @property long algoNormal() const nothrow @safe - { - return _snapshot.algoNormal; - } - + @property long algoNormal() const nothrow @safe { return _snapshot.algoNormal; } /// FastCDC: максимальный размер чанка. - @property long algoMax() const nothrow @safe - { - return _snapshot.algoMax; - } - + @property long algoMax() const nothrow @safe { return _snapshot.algoMax; } /// FastCDC: строгая маска. - @property long maskS() const nothrow @safe - { - return _snapshot.maskS; - } - + @property long maskS() const nothrow @safe { return _snapshot.maskS; } /// FastCDC: слабая маска. - @property long maskL() const nothrow @safe - { - return _snapshot.maskL; - } - + @property long maskL() const nothrow @safe { return _snapshot.maskL; } /// UID процесса (effective). - @property long uid() const nothrow @safe - { - return _snapshot.uid; - } - + @property long uid() const nothrow @safe { return _snapshot.uid; } /// Real UID процесса. - @property long ruid() const nothrow @safe - { - return _snapshot.ruid; - } - + @property long ruid() const nothrow @safe { return _snapshot.ruid; } /// Имя пользователя для `uid`. - @property string uidName() const nothrow @safe - { - return _snapshot.uidName; - } - + @property string uidName() const nothrow @safe { return _snapshot.uidName; } /// Имя пользователя для `ruid`. - @property string ruidName() const nothrow @safe - { - return _snapshot.ruidName; - } - + @property string ruidName() const nothrow @safe { return _snapshot.ruidName; } /// Имя процесса (из таблицы `processes`). - @property string process() const nothrow @safe - { - return _snapshot.process; - } - + @property string process() const nothrow @safe { return _snapshot.process.name; } /// Удобный флаг: снимок «готов». - @property bool isReady() const nothrow @safe - { - return _snapshot.status == SnapshotStatus.ready; - } - + @property bool isReady() const nothrow @safe { return _snapshot.status == SnapshotStatus.ready; } /// Удобный флаг: снимок «в процессе». - @property bool isPending() const nothrow @safe - { - return _snapshot.status == SnapshotStatus.pending; - } + @property bool isPending() const nothrow @safe { return _snapshot.status == SnapshotStatus.pending; } } diff --git a/source/cdcdb/storage.d b/source/cdcdb/storage.d index b9c3a90..5002057 100644 --- a/source/cdcdb/storage.d +++ b/source/cdcdb/storage.d @@ -1,12 +1,15 @@ module cdcdb.storage; import cdcdb.dblite; -import cdcdb.core; +import cdcdb.storagefile; import cdcdb.snapshot; +import cdcdb.core; +import cdcdb.lib : Identifier; import zstd : compress, Level; -/// Контекст создания снимка (идентификаторы и процесс). +import std.exception : enforce; + struct Context { long uid; /// UID процесса (effective). @@ -16,27 +19,6 @@ struct Context string process; /// Имя процесса. } -/** - * Высокоуровневый фасад для хранения: разбивает данные на чанки CDC, - * сохраняет чанки/блобы в SQLite через `DBLite`, связывает их в снимки - * и возвращает объекты `Snapshot` для последующего чтения и удаления. - * - * Возможности: - * - Разбиение FastCDC (контентно-зависимое, настраиваемые размеры/маски). - * - Опциональное сжатие Zstandard (уровень задаётся). - * - Идемпотентное создание снимков: пропускает, если последний снимок совпадает. - * - * Типичное использование: - * --- - * auto store = new Storage("base.db", true, Level.max); - * store.setupCDC(4096, 8192, 16384, 0x3FFF, 0x03FF); - * Context ctx; - * auto snap = store.newSnapshot("my.txt", data, ctx, "первичный импорт"); - * auto bytes = snap.data(); // восстановить содержимое - * - * auto removed = store.removeSnapshots("my.txt"); // удалить по имени файла - * --- - */ final class Storage { private: @@ -66,14 +48,6 @@ private: } public: - /// Конструктор: открывает/создаёт БД и подготавливает фасад. - /// - /// Параметры: - /// database = путь к файлу SQLite - /// zstd = включить Zstd-сжатие для блобов - /// level = уровень сжатия (см. `zstd.Level`) - /// busyTimeout = таймаут ожидания блокировки SQLite (мс) - /// maxRetries = число повторов при SQLITE_BUSY/LOCKED this(string database, bool zstd = false, int level = Level.base, size_t busyTimeout = 3000, size_t maxRetries = 3) { @@ -83,44 +57,27 @@ public: initCDC(); } - /// Перенастроить параметры CDC (влияет на будущие снимки). void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, size_t maskS, size_t maskL) { initCDC(minSize, normalSize, maxSize, maskS, maskL); } - /// Создаёт новый снимок из массива байт. - /// - /// - Разбивает данные по текущим параметрам FastCDC. - /// - Опционально сжимает чанки Zstd. - /// - Сохраняет уникальные блобы и связывает их со снимком. - /// - Если последний снимок для файла совпадает по SHA-256, возвращает `null`. - /// - /// Параметры: - /// file = имя файла (метка снимка) - /// data = содержимое файла - /// context = контекст (uid, ruid, процесс и т.д.) - /// description = необязательное описание - /// - /// Возвращает: объект `Snapshot` или `null` - /// - /// Исключения: при пустых данных или ошибках базы - Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, - string description = string.init) + Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init) { - if (data.length == 0) - { - throw new Exception("Данные имеют нулевую длину"); - } + enforce(data.length > 0, "Данные имеют нулевую длину"); + + auto dbFile = _db.getFile(file); import std.digest.sha : SHA256, digest; ubyte[32] sha256 = digest!SHA256(data); - // Если последний снимок совпадает — пропустить - if (_db.isLast(file, sha256)) + if (dbFile.empty) { + dbFile = _db.addFile(file); + } else if (_db.isLast(dbFile.id, sha256)) { return null; + } _db.beginImmediate(); bool ok; @@ -135,18 +92,22 @@ public: _db.commit(); } - // Запись пользователей/файлов/процессов _db.addUser(context.uid, context.uidName); if (context.uid != context.ruid) { _db.addUser(context.ruid, context.ruidName); } - _db.addFile(file); - _db.addProcess(context.process); + + auto dbProcess = _db.getProcess(context.process); + + if (dbProcess.empty) { + dbProcess = _db.addProcess(context.process); + } // Метаданные снимка DBSnapshot dbSnapshot; - dbSnapshot.file = file; + + dbSnapshot.file = dbFile; dbSnapshot.sha256 = sha256; dbSnapshot.description = description; dbSnapshot.sourceLength = data.length; @@ -157,9 +118,9 @@ public: dbSnapshot.maskL = _maskL; dbSnapshot.uid = context.uid; dbSnapshot.ruid = context.ruid; - dbSnapshot.process = context.process; + dbSnapshot.process = dbProcess; - auto idSnapshot = _db.addSnapshot(dbSnapshot); + enforce(_db.addSnapshot(dbSnapshot), "Не удалось добавить новый снимок в базу данных"); // Чанки и блобы DBSnapshotChunk dbSnapshotChunk; @@ -192,54 +153,99 @@ public: _db.addBlob(dbBlob); - dbSnapshotChunk.snapshotId = idSnapshot; + dbSnapshotChunk.snapshotId = dbSnapshot.id; dbSnapshotChunk.chunkIndex = chunk.index; dbSnapshotChunk.offset = chunk.offset; dbSnapshotChunk.sha256 = chunk.sha256; - _db.addSnapshotChunk(dbSnapshotChunk); + enforce(_db.addSnapshotChunk(dbSnapshotChunk), "Не удалось привязать снимок к данным"); } ok = true; - return new Snapshot(_db, idSnapshot); + return new Snapshot(_db, dbSnapshot); } - /// Удаляет все снимки по имени файла. - long removeSnapshots(string file) - { - return _db.deleteSnapshots(file); + StorageFile getFile(string path) { + auto dbFile = _db.getFile(path); + if (dbFile.empty) return null; + return new StorageFile(_db, dbFile); } - /// Удаляет конкретный снимок по объекту `Snapshot`. - bool removeSnapshot(Snapshot snapshot) - { - return removeSnapshot(snapshot.id); + StorageFile getFile(Identifier id) { + auto dbFile = _db.getFile(id); + if (dbFile.empty) return null; + return new StorageFile(_db, dbFile); } - /// Удаляет снимок по id. - bool removeSnapshot(long idSnapshot) - { - return _db.deleteSnapshot(idSnapshot) == idSnapshot; + StorageFile[] getFiles() { + StorageFile[] storageFiles; + foreach (dbFile; _db.getFiles()) { + storageFiles ~= new StorageFile(_db, dbFile); + } + return storageFiles; } - /// Возвращает `Snapshot` по id. - Snapshot getSnapshot(long idSnapshot) - { - return new Snapshot(_db, idSnapshot); + StorageFile[] findFile(string pattern) { + StorageFile[] storageFiles; + foreach (dbFile; _db.findFile(pattern)) { + storageFiles ~= new StorageFile(_db, dbFile); + } + return storageFiles; } - /// Возвращает список снимков (опционально фильтр по имени файла). - Snapshot[] getSnapshots(string file = string.init) - { + StorageFile[] findFile(Identifier id) { + StorageFile[] storageFiles; + foreach (dbFile; _db.findFile(id)) { + storageFiles ~= new StorageFile(_db, dbFile); + } + return storageFiles; + } + + Snapshot getSnapshot(Identifier id) { + DBSnapshot dbSnapshot = _db.getSnapshot(id); + if (dbSnapshot.empty) + return null; + return new Snapshot(_db, dbSnapshot); + } + + Snapshot[] getSnapshots(Identifier id) { Snapshot[] snapshots; - foreach (snapshot; _db.getSnapshots(file)) + foreach (dbSnapshot; _db.getSnapshots(id)) { - snapshots ~= new Snapshot(_db, snapshot); + snapshots ~= new Snapshot(_db, dbSnapshot); } return snapshots; } - /// Версия библиотеки. + Snapshot[] getSnapshots(string file) { + Snapshot[] snapshots; + foreach (dbSnapshot; _db.getSnapshots(file)) + { + snapshots ~= new Snapshot(_db, dbSnapshot); + } + return snapshots; + } + + Snapshot[] findSnapshot(Identifier id) { + Snapshot[] snapshots; + foreach (dbSnapshot; _db.findSnapshot(id)) { + snapshots ~= new Snapshot(_db, dbSnapshot); + } + return snapshots; + } + + bool deleteFile(Identifier id) { + return _db.deleteFile(id); + } + + bool deleteFile(string name) { + return _db.deleteFile(name); + } + + bool deleteSnapshot(Identifier id) { + return _db.deleteSnapshot(id); + } + string getVersion() const @safe nothrow { import cdcdb.version_ : cdcdbVersion; diff --git a/source/cdcdb/storagefile.d b/source/cdcdb/storagefile.d new file mode 100644 index 0000000..a2c8101 --- /dev/null +++ b/source/cdcdb/storagefile.d @@ -0,0 +1,25 @@ +module cdcdb.storagefile; + +import cdcdb.snapshot; +import cdcdb.lib; +import cdcdb.dblite; + +final class StorageFile { +private: + DBLite _db; + DBFile _dbfile; + +public: + this(DBLite dblite, DBFile dbfile) { _db = dblite; _dbfile = dbfile; } + + @property ref Identifier id() return { return _dbfile.id; } + @property string name() const nothrow @safe { return _dbfile.path; } + + Snapshot[] snapshots() { + Snapshot[] snapshots; + foreach (dbSnapshot; _db.getSnapshots(_dbfile.id)) { + snapshots ~= new Snapshot(_db, dbSnapshot); + } + return snapshots; + } +} diff --git a/source/cdcdb/version_.d b/source/cdcdb/version_.d index ac00177..b7cd2d0 100644 --- a/source/cdcdb/version_.d +++ b/source/cdcdb/version_.d @@ -1,3 +1,3 @@ module cdcdb.version_; -enum cdcdbVersion = "0.1.0"; +enum cdcdbVersion = "0.2.0"; diff --git a/test/app.d b/test/app.d index e71c394..821aee9 100644 --- a/test/app.d +++ b/test/app.d @@ -3,14 +3,14 @@ import std.stdio : writeln, File; import std.file : exists, remove, read; import zstd : Level; +import core.thread : Thread, msecs, dur; + void main() { // Создаем временную базу для примера string dbPath = "./bin/example.db"; - if (exists(dbPath)) { - remove(dbPath); - } + if (exists(dbPath)) { remove(dbPath); } Context context; @@ -31,15 +31,18 @@ void main() ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup; // Создание первого снимка - auto snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0"); + Snapshot snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0"); if (snap1) { writeln("Создан снимок с ID: ", snap1.id); writeln("Файл: ", snap1.file); writeln("Размер: ", snap1.length, " байт"); writeln("Статус: ", snap1.status); + writeln("Время: ", snap1.created); } + Thread.sleep( dur!("msecs")( 50 ) ); + // Создание второго снимка (обновление) auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0"); if (snap2) @@ -68,9 +71,9 @@ void main() writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData)); } - // Удаление снимков по метке - long deleted = storage.removeSnapshots("example_file"); - writeln("Удалено снимков: ", deleted); + // Удаление файла + if (storage.deleteFile("example_file")) + writeln("Файл example_file удален."); // Проверка: снимки удалены auto remaining = storage.getSnapshots("example_file"); From 0fc56e7c048f306b31383bfda0af8d296f7f2ab1 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 3 Oct 2025 19:35:51 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=B4=D1=80=D1=83=D0=B3=D1=83=D1=8E=20=D0=B1?= =?UTF-8?q?=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dub.json | 2 +- dub.selections.json | 1 + source/cdcdb/dblite.d | 299 +++++++++++++++++++++------------------- source/cdcdb/lib/hash.d | 12 ++ test/app.d | 14 +- 5 files changed, 180 insertions(+), 148 deletions(-) diff --git a/dub.json b/dub.json index ac9ccba..d435e7f 100644 --- a/dub.json +++ b/dub.json @@ -7,7 +7,7 @@ "license": "BSL-1.0", "name": "cdcdb", "dependencies": { - "arsd-official:sqlite": "~>12.0.0", + "d2sqlite3": "~>1.0.0", "zstd": "~>0.2.1" }, "stringImportPaths": [ diff --git a/dub.selections.json b/dub.selections.json index caa3e75..25dd8d4 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -2,6 +2,7 @@ "fileVersion": 1, "versions": { "arsd-official": "12.0.0", + "d2sqlite3": "1.0.0", "zstd": "0.2.1" } } diff --git a/source/cdcdb/dblite.d b/source/cdcdb/dblite.d index 9eb3906..7010505 100644 --- a/source/cdcdb/dblite.d +++ b/source/cdcdb/dblite.d @@ -1,6 +1,6 @@ module cdcdb.dblite; -import arsd.sqlite : Sqlite, SqliteResult, DatabaseException; +import d2sqlite3; import std.string : join, replace, toLower; import std.algorithm : canFind; @@ -91,19 +91,29 @@ struct DBSnapshotChunkData { ubyte[32] zSha256; /// Хеш сжатого содержимого. } -final class DBLite : Sqlite +final class DBLite { private: string _dbPath; /// Путь к файлу БД. size_t _maxRetries; /// Максимум повторов при `busy/locked`. + Database _db; /// Соединение с БД (d2sqlite3). + // SQL-схема (массив строковых запросов). mixin(import("scheme.d")); /// Выполняет SQL с повторными попытками при `locked/busy`. - SqliteResult sql(T...)(string queryText, T args) + ResultRange sql(T...)(string queryText, T args) { + // Готовим стейтмент сами, чтобы bindAll() работал и для BLOB. + auto attempt = () { + auto st = _db.prepare(queryText); + static if (T.length > 0) + st.bindAll(args); + return st.execute(); + }; + if (_maxRetries == 0) { - return cast(SqliteResult) query(queryText, args); + return attempt(); } string msg; @@ -111,10 +121,11 @@ private: while (tryNo) { try { - return cast(SqliteResult) query(queryText, args); - } catch (DatabaseException e) { + return attempt(); + } catch (SqliteException e) { msg = e.msg; - if (msg.toLower.canFind("locked", "busy")) { + const code = e.code; + if (code == SQLITE_BUSY || code == SQLITE_LOCKED) { if (--tryNo == 0) { throw new Exception( "Не удалось выполнить запрос к базе данных после %d неудачных попыток: %s" @@ -123,17 +134,18 @@ private: } continue; } - break; + break; // другие ошибки — дальше по стеку } } - throw new Exception(msg); + // До сюда не дойдём, но для формальной полноты: + throw new Exception(msg.length ? msg : "SQLite error"); } /// Проверяет наличие обязательных таблиц. /// Если все отсутствуют — создаёт схему; если отсутствует часть — бросает ошибку. void check() { - SqliteResult queryResult = sql( + auto queryResult = sql( q{ WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files")) @@ -151,7 +163,7 @@ private: foreach (row; queryResult) { - missingTables ~= row["missing_table"].to!string; + missingTables ~= row["missing_table"].as!string; } enforce(missingTables.length == 0 || missingTables.length == 6, @@ -171,16 +183,16 @@ public: this(string database, size_t busyTimeout, size_t maxRetries) { _dbPath = database; - super(database); + _db = Database(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)); + _db.execute("PRAGMA journal_mode=WAL"); + _db.execute("PRAGMA synchronous=NORMAL"); + _db.execute("PRAGMA foreign_keys=ON"); + _db.execute("PRAGMA busy_timeout=%d".format(busyTimeout)); } /// BEGIN IMMEDIATE. @@ -192,13 +204,13 @@ public: /// COMMIT. void commit() { - sql("COMMIT"); + _db.commit(); } /// ROLLBACK. void rollback() { - sql("ROLLBACK"); + _db.rollback(); } /************************************************* @@ -218,8 +230,8 @@ public: { DBFile file; - file.id = cast(ubyte[]) row["id"].dup; - file.path = row["name"].to!string; + file.id = row["id"].as!Blob(Blob.init); + file.path = row["name"].as!string; files ~= file; } @@ -236,10 +248,10 @@ public: DBFile file; - if (!queryResult.empty()) { - auto data = queryResult.front(); + if (!queryResult.empty) { + auto data = queryResult.front; - file.id = cast(ubyte[]) data["id"].dup; + file.id = data["id"].as!Blob(Blob.init); file.path = path; } @@ -257,18 +269,18 @@ public: DBFile file; - if (!queryResult.empty()) { - auto data = queryResult.front(); + if (!queryResult.empty) { + auto data = queryResult.front; file.id = id; - file.path = data["name"].to!string; + file.path = data["name"].as!string; } return file; } DBFile addFile(string path) { - SqliteResult queryResult; + ResultRange queryResult; UUID uuid; // Исключение одинакового UUID первичного ключа @@ -279,7 +291,7 @@ public: SELECT id FROM files WHERE id = ?1 }, uuid.data[] ); - } while (!queryResult.empty()); + } while (!queryResult.empty); queryResult = sql( q{ @@ -289,7 +301,7 @@ public: }, uuid.data[], path ); - enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); + enforce(!queryResult.empty, "Не удалось добавить новый файл в базу данных"); return DBFile(Identifier(uuid.data), path); } @@ -307,8 +319,8 @@ public: { DBFile file; - file.id = cast(ubyte[]) row["id"].dup; - file.path = row["name"].to!string; + file.id = row["id"].as!Blob(Blob.init); + file.path = row["name"].as!string; files ~= file; } @@ -339,8 +351,8 @@ public: { DBFile file; - file.id = cast(ubyte[]) row["id"].dup; - file.path = row["name"].to!string; + file.id = row["id"].as!Blob(Blob.init); + file.path = row["name"].as!string; files ~= file; } @@ -350,12 +362,12 @@ public: bool deleteFile(Identifier id) { auto queryResult = sql("DELETE FROM files WHERE id = ?1 RETURNING id", id[]); - return !queryResult.empty(); + return !queryResult.empty; } bool deleteFile(string path) { auto queryResult = sql("DELETE FROM files WHERE name = ?1 RETURNING id", path); - return !queryResult.empty(); + return !queryResult.empty; } ///////////////////////////////////////////////////////////////////// @@ -376,8 +388,8 @@ public: }, id[], sha256[] ); - if (!queryResult.empty()) - return queryResult.front()["is_last"].to!long > 0; + if (!queryResult.empty) + return queryResult.front["is_last"].as!long > 0; return false; } @@ -393,7 +405,7 @@ public: }, uid, name ); - return !queryResult.empty(); + return !queryResult.empty; } DBProcess getProcess(string name) { @@ -405,10 +417,10 @@ public: DBProcess process; - if (!queryResult.empty()) { - auto data = queryResult.front(); + if (!queryResult.empty) { + auto data = queryResult.front; - process.id = cast(ubyte[]) data["id"].dup; + process.id = data["id"].as!Blob(Blob.init); process.name = name; } @@ -426,18 +438,18 @@ public: DBProcess process; - if (!queryResult.empty()) { - auto data = queryResult.front(); + if (!queryResult.empty) { + auto data = queryResult.front; process.id = id; - process.name = data["name"].to!string; + process.name = data["name"].as!string; } return process; } DBProcess addProcess(string name) { - SqliteResult queryResult; + ResultRange queryResult; UUID uuid; // Исключение одинакового UUID первичного ключа @@ -448,7 +460,7 @@ public: SELECT id FROM processes WHERE id = ?1 }, uuid.data[] ); - } while (!queryResult.empty()); + } while (!queryResult.empty); queryResult = sql( q{ @@ -458,7 +470,7 @@ public: }, uuid.data[], name ); - enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); + enforce(!queryResult.empty, "Не удалось добавить новый файл в базу данных"); return DBProcess(Identifier(uuid.data), name); } @@ -467,7 +479,7 @@ public: bool addSnapshot(ref DBSnapshot snapshot) { - SqliteResult queryResult; + ResultRange queryResult; UUID uuid; // Исключение одинакового UUID первичного ключа @@ -478,7 +490,7 @@ public: SELECT id FROM snapshots WHERE id = ?1 }, uuid.data[] ); - } while (!queryResult.empty()); + } while (!queryResult.empty); import std.datetime : Clock; @@ -524,7 +536,7 @@ public: snapshot.status.to!int // ?15 ); - return !queryResult.empty(); + return !queryResult.empty; } ///////////////////////////////////////////////////////////////////// @@ -564,7 +576,7 @@ public: blob.zstd.to!int // ?8 ); - return !queryResult.empty(); + return !queryResult.empty; } bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) @@ -581,7 +593,7 @@ public: snapshotChunk.sha256[] ); - return !queryResult.empty(); + return !queryResult.empty; } DBSnapshotChunkData[] getChunks(Identifier id) @@ -603,14 +615,22 @@ public: { DBSnapshotChunkData sdch; - sdch.chunkIndex = row["chunk_index"].to!long; - sdch.offset = row["offset"].to!long; - sdch.size = row["size"].to!long; - sdch.content = cast(ubyte[]) row["content"].dup; - sdch.zstd = cast(bool) row["zstd"].to!int; - sdch.zSize = row["z_size"].to!long; - sdch.sha256 = cast(ubyte[]) row["sha256"].dup; - sdch.zSha256 = cast(ubyte[]) row["z_sha256"].dup; + sdch.chunkIndex = row["chunk_index"].as!long; + sdch.offset = row["offset"].as!long; + sdch.size = row["size"].as!long; + + // content может быть NULL + auto contentBlob = cast(ubyte[]) row["content"].as!Blob(Blob.init); + sdch.content = contentBlob.length ? contentBlob.dup : null; + + sdch.zstd = row["zstd"].as!int != 0; + sdch.zSize = row["z_size"].as!long; + + auto sha = cast(ubyte[]) row["sha256"].as!Blob(Blob.init); + if (sha.length) sdch.sha256[] = sha; + + auto zsha = cast(ubyte[]) row["z_sha256"].as!Blob(Blob.init); + if (zsha.length) sdch.zSha256[] = zsha; sdchs ~= sdch; } @@ -657,28 +677,28 @@ public: { DBSnapshot snapshot; - snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.id = row["id"].as!Blob(Blob.init); snapshot.file = DBFile( - Identifier(cast(ubyte[]) row["file_id"].dup), - row["file_name"].to!string + Identifier(row["file_id"].as!Blob(Blob.init)), + row["file_name"].as!string ); - snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; - snapshot.description = row["description"].to!string; - snapshot.createdUtc = row["created_utc"].to!long; - 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; - snapshot.uid = row["uid"].to!long; - snapshot.ruid = row["ruid"].to!long; - snapshot.uidName = row["uid_name"].to!string; - snapshot.ruidName = row["ruid_name"].to!string; + snapshot.sha256 = row["sha256"].as!Blob(Blob.init); + snapshot.description = row["description"].as!string(""); // может быть NULL + snapshot.createdUtc = row["created_utc"].as!long; + snapshot.sourceLength = row["source_length"].as!long; + snapshot.algoMin = row["algo_min"].as!long; + snapshot.algoNormal = row["algo_normal"].as!long; + snapshot.algoMax = row["algo_max"].as!long; + snapshot.maskS = row["mask_s"].as!long; + snapshot.maskL = row["mask_l"].as!long; + snapshot.status = cast(SnapshotStatus) row["status"].as!int; + snapshot.uid = row["uid"].as!long; + snapshot.ruid = row["ruid"].as!long; + snapshot.uidName = row["uid_name"].as!string; + snapshot.ruidName = row["ruid_name"].as!string; snapshot.process = DBProcess( - Identifier(cast(ubyte[]) row["process_id"].dup), - row["process_name"].to!string + Identifier(row["process_id"].as!Blob(Blob.init)), + row["process_name"].as!string ); snapshots ~= snapshot; @@ -727,30 +747,29 @@ public: { DBSnapshot snapshot; - snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.id = row["id"].as!Blob(Blob.init); snapshot.file = DBFile( - Identifier(cast(ubyte[]) row["file_id"].dup), - row["file_name"].to!string + Identifier(row["file_id"].as!Blob(Blob.init)), + row["file_name"].as!string ); - snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; - snapshot.description = row["description"].to!string; - snapshot.createdUtc = row["created_utc"].to!long; - 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; - snapshot.uid = row["uid"].to!long; - snapshot.ruid = row["ruid"].to!long; - snapshot.uidName = row["uid_name"].to!string; - snapshot.ruidName = row["ruid_name"].to!string; + snapshot.sha256 = row["sha256"].as!Blob(Blob.init); + snapshot.description = row["description"].as!string(""); + snapshot.createdUtc = row["created_utc"].as!long; + snapshot.sourceLength = row["source_length"].as!long; + snapshot.algoMin = row["algo_min"].as!long; + snapshot.algoNormal = row["algo_normal"].as!long; + snapshot.algoMax = row["algo_max"].as!long; + snapshot.maskS = row["mask_s"].as!long; + snapshot.maskL = row["mask_l"].as!long; + snapshot.status = cast(SnapshotStatus) row["status"].as!int; + snapshot.uid = row["uid"].as!long; + snapshot.ruid = row["ruid"].as!long; + snapshot.uidName = row["uid_name"].as!string; + snapshot.ruidName = row["ruid_name"].as!string; snapshot.process = DBProcess( - Identifier(cast(ubyte[]) row["process_id"].dup), - row["process_name"].to!string + Identifier(row["process_id"].as!Blob(Blob.init)), + row["process_name"].as!string ); - snapshots ~= snapshot; } @@ -793,31 +812,31 @@ public: DBSnapshot snapshot; - if (!queryResult.empty()) { - auto data = queryResult.front(); + if (!queryResult.empty) { + auto data = queryResult.front; - snapshot.id = cast(ubyte[]) data["id"].dup; + snapshot.id = data["id"].as!Blob(Blob.init); snapshot.file = DBFile( - Identifier(cast(ubyte[]) data["file_id"].dup), - data["file_name"].to!string + Identifier(data["file_id"].as!Blob(Blob.init)), + data["file_name"].as!string ); - snapshot.sha256 = cast(ubyte[]) data["sha256"].dup; - snapshot.description = data["description"].to!string; - snapshot.createdUtc = data["created_utc"].to!long; - 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; - snapshot.uid = data["uid"].to!long; - snapshot.ruid = data["ruid"].to!long; - snapshot.uidName = data["uid_name"].to!string; - snapshot.ruidName = data["ruid_name"].to!string; + snapshot.sha256 = data["sha256"].as!Blob(Blob.init); + snapshot.description = data["description"].as!string(""); + snapshot.createdUtc = data["created_utc"].as!long; + snapshot.sourceLength = data["source_length"].as!long; + snapshot.algoMin = data["algo_min"].as!long; + snapshot.algoNormal = data["algo_normal"].as!long; + snapshot.algoMax = data["algo_max"].as!long; + snapshot.maskS = data["mask_s"].as!long; + snapshot.maskL = data["mask_l"].as!long; + snapshot.status = cast(SnapshotStatus) data["status"].as!int; + snapshot.uid = data["uid"].as!long; + snapshot.ruid = data["ruid"].as!long; + snapshot.uidName = data["uid_name"].as!string; + snapshot.ruidName = data["ruid_name"].as!string; snapshot.process = DBProcess( - Identifier(cast(ubyte[]) data["process_id"].dup), - data["process_name"].to!string + Identifier(data["process_id"].as!Blob(Blob.init)), + data["process_name"].as!string ); } @@ -870,28 +889,28 @@ public: { DBSnapshot snapshot; - snapshot.id = cast(ubyte[]) row["id"].dup; + snapshot.id = row["id"].as!Blob(Blob.init); snapshot.file = DBFile( - Identifier(cast(ubyte[]) row["file_id"].dup), - row["file_name"].to!string + Identifier(row["file_id"].as!Blob(Blob.init)), + row["file_name"].as!string ); - snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; - snapshot.description = row["description"].to!string; - snapshot.createdUtc = row["created_utc"].to!long; - 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; - snapshot.uid = row["uid"].to!long; - snapshot.ruid = row["ruid"].to!long; - snapshot.uidName = row["uid_name"].to!string; - snapshot.ruidName = row["ruid_name"].to!string; + snapshot.sha256 = row["sha256"].as!Blob(Blob.init); + snapshot.description = row["description"].as!string(""); + snapshot.createdUtc = row["created_utc"].as!long; + snapshot.sourceLength = row["source_length"].as!long; + snapshot.algoMin = row["algo_min"].as!long; + snapshot.algoNormal = row["algo_normal"].as!long; + snapshot.algoMax = row["algo_max"].as!long; + snapshot.maskS = row["mask_s"].as!long; + snapshot.maskL = row["mask_l"].as!long; + snapshot.status = cast(SnapshotStatus) row["status"].as!int; + snapshot.uid = row["uid"].as!long; + snapshot.ruid = row["ruid"].as!long; + snapshot.uidName = row["uid_name"].as!string; + snapshot.ruidName = row["ruid_name"].as!string; snapshot.process = DBProcess( - Identifier(cast(ubyte[]) row["process_id"].dup), - row["process_name"].to!string + Identifier(row["process_id"].as!Blob(Blob.init)), + row["process_name"].as!string ); snapshots ~= snapshot; @@ -902,6 +921,6 @@ public: bool deleteSnapshot(Identifier id) { auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id[]); - return !queryResult.empty(); + return !queryResult.empty; } } diff --git a/source/cdcdb/lib/hash.d b/source/cdcdb/lib/hash.d index 4303c71..ed58887 100644 --- a/source/cdcdb/lib/hash.d +++ b/source/cdcdb/lib/hash.d @@ -60,12 +60,24 @@ public: _data = data; } + this(immutable(ubyte[]) data) + { + assert(data.length <= 16); + _data = data.dup; + } + this(ref const ubyte[16] data) { assert(data.length <= 16); _data = data.dup; } + void opAssign(immutable(ubyte[]) data) + { + assert(data.length <= 16); + _data = data.dup; + } + void opAssign(ubyte[] data) { assert(data.length <= 16); diff --git a/test/app.d b/test/app.d index 821aee9..d48a384 100644 --- a/test/app.d +++ b/test/app.d @@ -72,13 +72,13 @@ void main() } // Удаление файла - if (storage.deleteFile("example_file")) - writeln("Файл example_file удален."); + // if (storage.deleteFile("example_file")) + // writeln("Файл example_file удален."); - // Проверка: снимки удалены - auto remaining = storage.getSnapshots("example_file"); - assert(remaining.length == 0); - writeln("Все снимки удалены."); + // // Проверка: снимки удалены + // auto remaining = storage.getSnapshots("example_file"); + // assert(remaining.length == 0); + // writeln("Все снимки удалены."); - writeln("Версия библиотеки: ", storage.getVersion()); + // writeln("Версия библиотеки: ", storage.getVersion()); } From 578e6a3358aa670543a220b73865f5f44e52a2a7 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Fri, 24 Oct 2025 16:30:49 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9:=20sql=20->=20?= =?UTF-8?q?execute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/cdcdb/dblite.d | 2 +- test/app.d | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/cdcdb/dblite.d b/source/cdcdb/dblite.d index 7010505..b0514e1 100644 --- a/source/cdcdb/dblite.d +++ b/source/cdcdb/dblite.d @@ -198,7 +198,7 @@ public: /// BEGIN IMMEDIATE. void beginImmediate() { - sql("BEGIN IMMEDIATE"); + _db.execute("BEGIN IMMEDIATE"); } /// COMMIT. diff --git a/test/app.d b/test/app.d index d48a384..821aee9 100644 --- a/test/app.d +++ b/test/app.d @@ -72,13 +72,13 @@ void main() } // Удаление файла - // if (storage.deleteFile("example_file")) - // writeln("Файл example_file удален."); + if (storage.deleteFile("example_file")) + writeln("Файл example_file удален."); - // // Проверка: снимки удалены - // auto remaining = storage.getSnapshots("example_file"); - // assert(remaining.length == 0); - // writeln("Все снимки удалены."); + // Проверка: снимки удалены + auto remaining = storage.getSnapshots("example_file"); + assert(remaining.length == 0); + writeln("Все снимки удалены."); - // writeln("Версия библиотеки: ", storage.getVersion()); + writeln("Версия библиотеки: ", storage.getVersion()); }