Добавлен таймаут для запросов в БД. Запрет на обновление таблиц в БД, кроме разрешенных полей

This commit is contained in:
Alexander Zhirov 2025-09-12 15:08:32 +03:00
parent 4f735abae7
commit 9ba43b0e38
Signed by: alexander
GPG key ID: C8D8BE544A27C511
5 changed files with 121 additions and 44 deletions

View file

@ -32,13 +32,15 @@ public:
this( this(
string database, string database,
bool zstd = false, bool zstd = false,
int busyTimeout = 3000,
ubyte maxRetries = 3,
size_t minSize = 256, size_t minSize = 256,
size_t normalSize = 512, size_t normalSize = 512,
size_t maxSize = 1024, size_t maxSize = 1024,
size_t maskS = 0xFF, size_t maskS = 0xFF,
size_t maskL = 0x0F size_t maskL = 0x0F
) { ) {
_db = new DBLite(database); _db = new DBLite(database, busyTimeout, maxRetries);
_zstd = zstd; _zstd = zstd;
_minSize = minSize; _minSize = minSize;
@ -50,20 +52,18 @@ public:
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL); _cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
} }
size_t newSnapshot(string filePath, const(ubyte)[] data, string label = string.init) size_t newSnapshot(string label, const(ubyte)[] data, string description = string.init)
{ {
ubyte[32] hashSource = digest!SHA256(data); ubyte[32] sha256 = digest!SHA256(data);
// Если последний снимок файла соответствует текущему // Если последний снимок файла соответствует текущему состоянию
if (_db.isLast(filePath, hashSource)) return 0; if (_db.isLast(label, sha256)) return 0;
// Разбить на фрагменты
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.algoMin = _minSize;
snapshot.algoNormal = _normalSize; snapshot.algoNormal = _normalSize;
@ -92,6 +92,9 @@ public:
blob.zstd = _zstd; blob.zstd = _zstd;
// Разбить на фрагменты
auto chunks = _cdc.split(data);
// Запись фрагментов в БД // Запись фрагментов в БД
foreach (chunk; chunks) foreach (chunk; chunks)
{ {
@ -150,7 +153,7 @@ public:
enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому"); enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
content ~= bytes; content ~= bytes;
} }
enforce(snapshot.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает"); enforce(snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
return content; return content;
} }

View file

@ -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.label,
snapshot.fileSha256[], snapshot.sha256[],
snapshot.label.length ? snapshot.label : null, 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;

View file

@ -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
} }
]; ];

View file

@ -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;

View file

@ -10,7 +10,7 @@ void main()
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);
} }