Обновлен CAS, добавлены проверки
This commit is contained in:
		
							parent
							
								
									9c5d812a49
								
							
						
					
					
						commit
						b29b328f91
					
				
					 4 changed files with 161 additions and 117 deletions
				
			
		| 
						 | 
				
			
			@ -14,12 +14,34 @@ import std.conv : to;
 | 
			
		|||
 | 
			
		||||
import std.file : write;
 | 
			
		||||
 | 
			
		||||
// CAS-хранилище (Content-Addressable Storage) со снапшотами
 | 
			
		||||
// Content-Addressable Storage (Контентно-адресуемая система хранения)
 | 
			
		||||
// CAS-хранилище со снапшотами
 | 
			
		||||
final class CAS
 | 
			
		||||
{
 | 
			
		||||
private:
 | 
			
		||||
	DBLite _db;
 | 
			
		||||
	bool _zstd;
 | 
			
		||||
 | 
			
		||||
	ubyte[] buildContent(const ref Snapshot snapshot)
 | 
			
		||||
	{
 | 
			
		||||
		auto dataChunks = _db.getChunks(snapshot.id);
 | 
			
		||||
		ubyte[] content;
 | 
			
		||||
 | 
			
		||||
		foreach (chunk; dataChunks) {
 | 
			
		||||
			ubyte[] bytes;
 | 
			
		||||
			if (chunk.zstd) {
 | 
			
		||||
				enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
 | 
			
		||||
				bytes = cast(ubyte[]) uncompress(chunk.content);
 | 
			
		||||
			} else {
 | 
			
		||||
				bytes = chunk.content;
 | 
			
		||||
			}
 | 
			
		||||
			enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
 | 
			
		||||
			content ~= bytes;
 | 
			
		||||
		}
 | 
			
		||||
		enforce(snapshot.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
			
		||||
 | 
			
		||||
		return content;
 | 
			
		||||
	}
 | 
			
		||||
public:
 | 
			
		||||
	this(string database, bool zstd = false)
 | 
			
		||||
	{
 | 
			
		||||
| 
						 | 
				
			
			@ -27,14 +49,12 @@ public:
 | 
			
		|||
		_zstd = zstd;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	size_t saveSnapshot(string filePath, const(ubyte)[] data)
 | 
			
		||||
	size_t newSnapshot(string filePath, string label, const(ubyte)[] data)
 | 
			
		||||
	{
 | 
			
		||||
		ubyte[32] hashSource = digest!SHA256(data);
 | 
			
		||||
		// Сделать запрос в БД по filePath и сверить хеш файлов
 | 
			
		||||
 | 
			
		||||
		// writeln(hashSource.length);
 | 
			
		||||
		
 | 
			
		||||
 | 
			
		||||
		if (_db.isLast(filePath, hashSource)) return 0;
 | 
			
		||||
 | 
			
		||||
		// Параметры для CDC вынести в отдельные настройки (продумать)
 | 
			
		||||
		auto cdc = new CDC(256, 512, 1024, 0xFF, 0x0F);
 | 
			
		||||
| 
						 | 
				
			
			@ -42,13 +62,26 @@ public:
 | 
			
		|||
		auto chunks = cdc.split(data);
 | 
			
		||||
 | 
			
		||||
		Snapshot snapshot;
 | 
			
		||||
 | 
			
		||||
		snapshot.filePath = filePath;
 | 
			
		||||
		snapshot.fileSha256 = hashSource;
 | 
			
		||||
		snapshot.label = "Файл для теста";
 | 
			
		||||
		snapshot.label = label;
 | 
			
		||||
		snapshot.sourceLength = data.length;
 | 
			
		||||
 | 
			
		||||
		_db.beginImmediate();
 | 
			
		||||
 | 
			
		||||
		bool ok;
 | 
			
		||||
 | 
			
		||||
		scope (exit)
 | 
			
		||||
		{
 | 
			
		||||
			if (!ok)
 | 
			
		||||
				_db.rollback();
 | 
			
		||||
		}
 | 
			
		||||
		scope (success)
 | 
			
		||||
		{
 | 
			
		||||
			_db.commit();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto idSnapshot = _db.addSnapshot(snapshot);
 | 
			
		||||
 | 
			
		||||
		SnapshotChunk snapshotChunk;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +89,7 @@ public:
 | 
			
		|||
 | 
			
		||||
		blob.zstd = _zstd;
 | 
			
		||||
 | 
			
		||||
		// Записать фрагменты в БД
 | 
			
		||||
		// Запись фрагментов в БД
 | 
			
		||||
		foreach (chunk; chunks)
 | 
			
		||||
		{
 | 
			
		||||
			blob.sha256 = chunk.sha256;
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +109,7 @@ public:
 | 
			
		|||
				blob.content = content.dup;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Запись фрагментов
 | 
			
		||||
			_db.addBlob(blob);
 | 
			
		||||
 | 
			
		||||
			snapshotChunk.snapshotId = idSnapshot;
 | 
			
		||||
| 
						 | 
				
			
			@ -83,38 +117,28 @@ public:
 | 
			
		|||
			snapshotChunk.offset = chunk.offset;
 | 
			
		||||
			snapshotChunk.sha256 = chunk.sha256;
 | 
			
		||||
 | 
			
		||||
			// Привязка фрагментов к снимку
 | 
			
		||||
			_db.addSnapshotChunk(snapshotChunk);
 | 
			
		||||
		}
 | 
			
		||||
		_db.commit();
 | 
			
		||||
		// Записать манифест в БД
 | 
			
		||||
		// Вернуть ID манифеста
 | 
			
		||||
		return 0;
 | 
			
		||||
 | 
			
		||||
		ok = true;
 | 
			
		||||
 | 
			
		||||
		return idSnapshot;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void restoreSnapshot()
 | 
			
		||||
	Snapshot[] getSnapshotList(string filePath)
 | 
			
		||||
	{
 | 
			
		||||
		string restoreFile = "/tmp/restore.d";
 | 
			
		||||
 | 
			
		||||
		foreach (Snapshot snapshot; _db.getSnapshots("/tmp/text")) {
 | 
			
		||||
			auto dataChunks = _db.getChunks(snapshot.id);
 | 
			
		||||
			ubyte[] content;
 | 
			
		||||
 | 
			
		||||
			foreach (SnapshotDataChunk chunk; dataChunks) {
 | 
			
		||||
				ubyte[] bytes;
 | 
			
		||||
				if (chunk.zstd) {
 | 
			
		||||
					enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
 | 
			
		||||
					bytes = cast(ubyte[]) uncompress(chunk.content);
 | 
			
		||||
				} else {
 | 
			
		||||
					bytes = chunk.content;
 | 
			
		||||
				}
 | 
			
		||||
				enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
 | 
			
		||||
 | 
			
		||||
				content ~= bytes;
 | 
			
		||||
		return _db.getSnapshots(filePath);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			enforce(snapshot.fileSha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
			
		||||
	ubyte[] getSnapshotData(const ref Snapshot snapshot)
 | 
			
		||||
	{
 | 
			
		||||
		ubyte[] content = buildContent(snapshot);
 | 
			
		||||
		return content;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			write(snapshot.filePath ~ snapshot.id.to!string, content);
 | 
			
		||||
		}
 | 
			
		||||
	void removeSnapshot(const ref Snapshot snapshot)
 | 
			
		||||
	{
 | 
			
		||||
		_db.deleteSnapshot(snapshot.id);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@ import cdcdb.cdc.types;
 | 
			
		|||
 | 
			
		||||
import std.digest.sha : SHA256, digest;
 | 
			
		||||
 | 
			
		||||
// Change Data Capture
 | 
			
		||||
// Change Data Capture (Захват изменения данных)
 | 
			
		||||
final class CDC
 | 
			
		||||
{
 | 
			
		||||
private:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,13 @@
 | 
			
		|||
module cdcdb.db.dblite;
 | 
			
		||||
 | 
			
		||||
import cdcdb.db.types;
 | 
			
		||||
 | 
			
		||||
import arsd.sqlite;
 | 
			
		||||
import std.file : exists, isFile;
 | 
			
		||||
 | 
			
		||||
import std.file : exists;
 | 
			
		||||
import std.exception : enforce;
 | 
			
		||||
import std.conv : to;
 | 
			
		||||
 | 
			
		||||
import cdcdb.db.types;
 | 
			
		||||
import std.string : join;
 | 
			
		||||
 | 
			
		||||
final class DBLite : Sqlite
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -18,16 +20,50 @@ private:
 | 
			
		|||
	{
 | 
			
		||||
		return cast(SqliteResult) query(queryText, args);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Проверка БД на наличие существующих в ней необходимых таблиц
 | 
			
		||||
	void check()
 | 
			
		||||
	{
 | 
			
		||||
		SqliteResult queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"))
 | 
			
		||||
				SELECT name AS missing_table
 | 
			
		||||
				FROM required
 | 
			
		||||
				WHERE NOT EXISTS (
 | 
			
		||||
					SELECT 1
 | 
			
		||||
					FROM sqlite_master
 | 
			
		||||
					WHERE type = "table" AND name = required.name
 | 
			
		||||
				);
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		string[] missingTables;
 | 
			
		||||
 | 
			
		||||
		foreach (row; queryResult)
 | 
			
		||||
		{
 | 
			
		||||
			missingTables ~= row["missing_table"].to!string;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		enforce(missingTables.length == 0 || missingTables.length == 3,
 | 
			
		||||
			"База данных повреждена. Отсутствуют таблицы: " ~ missingTables.join(", ")
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (missingTables.length == 3)
 | 
			
		||||
		{
 | 
			
		||||
			foreach (schemeQuery; _scheme)
 | 
			
		||||
			{
 | 
			
		||||
				sql(schemeQuery);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	this(string database)
 | 
			
		||||
	{
 | 
			
		||||
		_dbPath = database;
 | 
			
		||||
		super(database);
 | 
			
		||||
 | 
			
		||||
		foreach (schemeQuery; _scheme)
 | 
			
		||||
		{
 | 
			
		||||
			sql(schemeQuery);
 | 
			
		||||
		}
 | 
			
		||||
		check();
 | 
			
		||||
 | 
			
		||||
		query("PRAGMA journal_mode=WAL");
 | 
			
		||||
		query("PRAGMA synchronous=NORMAL");
 | 
			
		||||
| 
						 | 
				
			
			@ -49,16 +85,6 @@ public:
 | 
			
		|||
		query("ROLLBACK");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Snapshot getSnapshot(string filePath, immutable ubyte[32] sha256)
 | 
			
		||||
	// {
 | 
			
		||||
	// 	auto queryResult = sql(
 | 
			
		||||
	// 		q{
 | 
			
		||||
	// 			SELECT * FROM snapshots
 | 
			
		||||
	// 			WHERE file_path = ? AND file_sha256 = ?
 | 
			
		||||
	// 		}, filePath, sha256
 | 
			
		||||
	// 	);
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	long addSnapshot(Snapshot snapshot)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
| 
						 | 
				
			
			@ -125,66 +151,23 @@ public:
 | 
			
		|||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// struct ChunkInput
 | 
			
		||||
	// {
 | 
			
		||||
	// 	long index;
 | 
			
		||||
	// 	long offset;
 | 
			
		||||
	// 	long size;
 | 
			
		||||
	// 	ubyte[32] sha256;
 | 
			
		||||
	// 	const(ubyte)[] content;
 | 
			
		||||
	// }
 | 
			
		||||
	bool isLast(string filePath, ubyte[] fileSha256) {
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT COALESCE(
 | 
			
		||||
					(SELECT (file_path = ? AND file_sha256 = ?)
 | 
			
		||||
					FROM snapshots
 | 
			
		||||
					ORDER BY created_utc DESC
 | 
			
		||||
					LIMIT 1),
 | 
			
		||||
					0
 | 
			
		||||
				) AS is_last;
 | 
			
		||||
			}, filePath, fileSha256
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
	// long saveSnapshotWithChunks(
 | 
			
		||||
	// 	string filePath, string label, long sourceLength,
 | 
			
		||||
	// 	long algoMin, long algoNormal, long algoMax,
 | 
			
		||||
	// 	long maskS, long maskL,
 | 
			
		||||
	// 	const ChunkInput[] chunks
 | 
			
		||||
	// )
 | 
			
		||||
	// {
 | 
			
		||||
	// 	beginImmediate();
 | 
			
		||||
 | 
			
		||||
	// 	bool ok;
 | 
			
		||||
 | 
			
		||||
	// 	scope (exit)
 | 
			
		||||
	// 	{
 | 
			
		||||
	// 		if (!ok)
 | 
			
		||||
	// 			rollback();
 | 
			
		||||
	// 	}
 | 
			
		||||
	// 	scope (success)
 | 
			
		||||
	// 	{
 | 
			
		||||
	// 		commit();
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	const snapId = insertSnapshotMeta(
 | 
			
		||||
	// 		filePath, label, sourceLength,
 | 
			
		||||
	// 		algoMin, algoNormal, algoMax,
 | 
			
		||||
	// 		maskS, maskL, SnapshotStatus.pending
 | 
			
		||||
	// 	);
 | 
			
		||||
 | 
			
		||||
	// 	foreach (c; chunks)
 | 
			
		||||
	// 	{
 | 
			
		||||
	// 		insertBlobIfMissing(c.sha256, c.size, c.content);
 | 
			
		||||
	// 		insertSnapshotChunk(snapId, c.index, c.offset, c.size, c.sha256);
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	ok = true;
 | 
			
		||||
 | 
			
		||||
	// 	return snapId;
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	// // --- чтение ---
 | 
			
		||||
		if (!queryResult.empty())
 | 
			
		||||
			return queryResult.front()["is_last"].to!long > 0;
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Snapshot[] getSnapshots(string filePath)
 | 
			
		||||
	{
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +180,7 @@ public:
 | 
			
		|||
		);
 | 
			
		||||
 | 
			
		||||
		Snapshot[] snapshots;
 | 
			
		||||
		// bool found = false;
 | 
			
		||||
 | 
			
		||||
		foreach (row; queryResult)
 | 
			
		||||
		{
 | 
			
		||||
			Snapshot snapshot;
 | 
			
		||||
| 
						 | 
				
			
			@ -214,14 +197,52 @@ public:
 | 
			
		|||
			snapshot.maskS = row["mask_s"].to!long;
 | 
			
		||||
			snapshot.maskL = row["mask_l"].to!long;
 | 
			
		||||
			snapshot.status = cast(SnapshotStatus) row["status"].to!int;
 | 
			
		||||
			// found = true;
 | 
			
		||||
 | 
			
		||||
			snapshots ~= snapshot;
 | 
			
		||||
		}
 | 
			
		||||
		// enforce(found, "getSnapshot: not found");
 | 
			
		||||
 | 
			
		||||
		return snapshots;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SnapshotDataChunk[] getChunks(long snapshotId) {
 | 
			
		||||
	Snapshot getSnapshot(long id)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT id, file_path, file_sha256, label, created_utc, source_length,
 | 
			
		||||
					algo_min, algo_normal, algo_max, mask_s, mask_l, status
 | 
			
		||||
				FROM snapshots WHERE id = ?
 | 
			
		||||
			}, id
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		Snapshot snapshot;
 | 
			
		||||
 | 
			
		||||
		if (!queryResult.empty())
 | 
			
		||||
		{
 | 
			
		||||
			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.createdUtc = 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;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return snapshot;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void deleteSnapshot(long id) {
 | 
			
		||||
		sql("DELETE FROM snapshots WHERE id = ?", id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SnapshotDataChunk[] getChunks(long snapshotId)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT sc.chunk_index, sc.offset,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,5 @@ import std.file : read;
 | 
			
		|||
void main()
 | 
			
		||||
{
 | 
			
		||||
	auto cas = new CAS("/tmp/base.db", true);
 | 
			
		||||
	cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
			
		||||
	// cas.restoreSnapshot();
 | 
			
		||||
	cas.newSnapshot("/tmp/text", "Файл для тестирования", cast(ubyte[]) read("/tmp/text"));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue