Добавлен таймаут для запросов в БД. Запрет на обновление таблиц в БД, кроме разрешенных полей
This commit is contained in:
		
							parent
							
								
									4f735abae7
								
							
						
					
					
						commit
						9ba43b0e38
					
				
					 5 changed files with 121 additions and 44 deletions
				
			
		| 
						 | 
				
			
			@ -32,13 +32,15 @@ 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);
 | 
			
		||||
		_db = new DBLite(database, busyTimeout, maxRetries);
 | 
			
		||||
		_zstd = zstd;
 | 
			
		||||
 | 
			
		||||
		_minSize = minSize;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,20 +52,18 @@ public:
 | 
			
		|||
		_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;
 | 
			
		||||
		// Разбить на фрагменты
 | 
			
		||||
		auto chunks = _cdc.split(data);
 | 
			
		||||
		// Если последний снимок файла соответствует текущему состоянию
 | 
			
		||||
		if (_db.isLast(label, sha256)) return 0;
 | 
			
		||||
 | 
			
		||||
		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;
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +92,9 @@ public:
 | 
			
		|||
 | 
			
		||||
		blob.zstd = _zstd;
 | 
			
		||||
 | 
			
		||||
		// Разбить на фрагменты
 | 
			
		||||
		auto chunks = _cdc.split(data);
 | 
			
		||||
 | 
			
		||||
		// Запись фрагментов в БД
 | 
			
		||||
		foreach (chunk; chunks)
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +153,7 @@ public:
 | 
			
		|||
			enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
 | 
			
		||||
			content ~= bytes;
 | 
			
		||||
		}
 | 
			
		||||
		enforce(snapshot.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
			
		||||
		enforce(snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
			
		||||
 | 
			
		||||
		return content;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,20 +7,47 @@ import arsd.sqlite;
 | 
			
		|||
import std.file : exists;
 | 
			
		||||
import std.exception : enforce;
 | 
			
		||||
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
 | 
			
		||||
{
 | 
			
		||||
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);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Проверка БД на наличие существующих в ней необходимых таблиц
 | 
			
		||||
	void check()
 | 
			
		||||
	{
 | 
			
		||||
| 
						 | 
				
			
			@ -64,31 +91,34 @@ private:
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	this(string database)
 | 
			
		||||
	this(string database, int busyTimeout, ubyte maxRetries)
 | 
			
		||||
	{
 | 
			
		||||
		_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()
 | 
			
		||||
	{
 | 
			
		||||
		query("BEGIN IMMEDIATE");
 | 
			
		||||
		sql("BEGIN IMMEDIATE");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void commit()
 | 
			
		||||
	{
 | 
			
		||||
		query("COMMIT");
 | 
			
		||||
		sql("COMMIT");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void rollback()
 | 
			
		||||
	{
 | 
			
		||||
		query("ROLLBACK");
 | 
			
		||||
		sql("ROLLBACK");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	long addSnapshot(Snapshot snapshot)
 | 
			
		||||
| 
						 | 
				
			
			@ -96,9 +126,9 @@ public:
 | 
			
		|||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				INSERT INTO snapshots(
 | 
			
		||||
					file_path,
 | 
			
		||||
					file_sha256,
 | 
			
		||||
					label,
 | 
			
		||||
					sha256,
 | 
			
		||||
					description,
 | 
			
		||||
					source_length,
 | 
			
		||||
					algo_min,
 | 
			
		||||
					algo_normal,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,9 +139,9 @@ public:
 | 
			
		|||
				) VALUES (?,?,?,?,?,?,?,?,?,?)
 | 
			
		||||
				RETURNING id
 | 
			
		||||
			},
 | 
			
		||||
			snapshot.filePath,
 | 
			
		||||
			snapshot.fileSha256[],
 | 
			
		||||
			snapshot.label.length ? snapshot.label : null,
 | 
			
		||||
			snapshot.label,
 | 
			
		||||
			snapshot.sha256[],
 | 
			
		||||
			snapshot.description.length ? snapshot.description : null,
 | 
			
		||||
			snapshot.sourceLength,
 | 
			
		||||
			snapshot.algoMin,
 | 
			
		||||
			snapshot.algoNormal,
 | 
			
		||||
| 
						 | 
				
			
			@ -157,17 +187,17 @@ public:
 | 
			
		|||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool isLast(string filePath, ubyte[] fileSha256) {
 | 
			
		||||
	bool isLast(string label, ubyte[] sha256) {
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT COALESCE(
 | 
			
		||||
					(SELECT (file_path = ? AND file_sha256 = ?)
 | 
			
		||||
					(SELECT (label = ? AND sha256 = ?)
 | 
			
		||||
					FROM snapshots
 | 
			
		||||
					ORDER BY created_utc DESC
 | 
			
		||||
					LIMIT 1),
 | 
			
		||||
					0
 | 
			
		||||
				) AS is_last;
 | 
			
		||||
			}, filePath, fileSha256
 | 
			
		||||
			}, label, sha256
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!queryResult.empty())
 | 
			
		||||
| 
						 | 
				
			
			@ -175,14 +205,14 @@ public:
 | 
			
		|||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Snapshot[] getSnapshots(string filePath)
 | 
			
		||||
	Snapshot[] getSnapshots(string label)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			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
 | 
			
		||||
				FROM snapshots WHERE file_path = ?
 | 
			
		||||
			}, filePath
 | 
			
		||||
				FROM snapshots WHERE label = ?
 | 
			
		||||
			}, label
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		Snapshot[] snapshots;
 | 
			
		||||
| 
						 | 
				
			
			@ -192,9 +222,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;
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +244,7 @@ public:
 | 
			
		|||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			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
 | 
			
		||||
				FROM snapshots WHERE id = ?
 | 
			
		||||
			}, id
 | 
			
		||||
| 
						 | 
				
			
			@ -227,9 +257,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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,12 +6,12 @@ auto _scheme = [
 | 
			
		|||
		CREATE TABLE IF NOT EXISTS snapshots (
 | 
			
		||||
			-- идентификатор снимка
 | 
			
		||||
			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)
 | 
			
		||||
			created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
			
		||||
			-- длина исходного файла в байтах
 | 
			
		||||
| 
						 | 
				
			
			@ -88,9 +88,9 @@ auto _scheme = [
 | 
			
		|||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс для запросов вида: WHERE file_path=? AND file_sha256=?
 | 
			
		||||
		-- Индекс для запросов вида: WHERE label=? AND sha256=?
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
 | 
			
		||||
			ON snapshots(file_path, file_sha256)
 | 
			
		||||
			ON snapshots(label, sha256)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс для обратного поиска использования blob по sha256
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +203,51 @@ auto _scheme = [
 | 
			
		|||
		CREATE TRIGGER IF NOT EXISTS trg_sc_block_update
 | 
			
		||||
		BEFORE UPDATE ON snapshot_chunks
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ void main()
 | 
			
		|||
	cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
			
		||||
	// import std.stdio : writeln;
 | 
			
		||||
 | 
			
		||||
	// writeln(cas.getSnapshotList("/tmp/text"));
 | 
			
		||||
	writeln(cas.getSnapshotList("/tmp/text"));
 | 
			
		||||
 | 
			
		||||
	// writeln(cas.getVersion);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue