forked from dlang/cdcdb
		
	Сохранение снимка
This commit is contained in:
		
							parent
							
								
									18bcf3742d
								
							
						
					
					
						commit
						8631c65e39
					
				
					 7 changed files with 419 additions and 26 deletions
				
			
		| 
						 | 
					@ -6,36 +6,80 @@ import cdcdb.cdc.core;
 | 
				
			||||||
import std.digest.sha : SHA256, digest;
 | 
					import std.digest.sha : SHA256, digest;
 | 
				
			||||||
import std.format : format;
 | 
					import std.format : format;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import zstd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CAS-хранилище (Content-Addressable Storage) со снапшотами
 | 
					// CAS-хранилище (Content-Addressable Storage) со снапшотами
 | 
				
			||||||
final class CAS
 | 
					final class CAS
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
private:
 | 
					private:
 | 
				
			||||||
	DBLite _db;
 | 
						DBLite _db;
 | 
				
			||||||
 | 
						bool _zstd;
 | 
				
			||||||
public:
 | 
					public:
 | 
				
			||||||
	this(string database)
 | 
						this(string database, bool zstd = false)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		_db = new DBLite(database);
 | 
							_db = new DBLite(database);
 | 
				
			||||||
 | 
							_zstd = zstd;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	size_t saveSnapshot(string filePath, const(ubyte)[] data)
 | 
						size_t saveSnapshot(string filePath, const(ubyte)[] data)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		ubyte[32] hashSource = digest!SHA256(data);
 | 
							ubyte[32] hashSource = digest!SHA256(data);
 | 
				
			||||||
		// Сделать запрос в БД по filePath и сверить хеш файлов
 | 
							// Сделать запрос в БД по filePath и сверить хеш файлов
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							import std.stdio : writeln;
 | 
				
			||||||
 | 
							// writeln(hashSource.length);
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Параметры для CDC вынести в отдельные настройки (продумать)
 | 
							// Параметры для CDC вынести в отдельные настройки (продумать)
 | 
				
			||||||
		auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F);
 | 
							auto cdc = new CDC(300, 700, 1000, 0xFF, 0x0F);
 | 
				
			||||||
		// Разбить на фрагменты
 | 
							// Разбить на фрагменты
 | 
				
			||||||
		auto chunks = cdc.split(data);
 | 
							auto chunks = cdc.split(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		import std.stdio : writeln;
 | 
							Snapshot snapshot;
 | 
				
			||||||
 | 
							snapshot.filePath = filePath;
 | 
				
			||||||
 | 
							snapshot.fileSha256 = hashSource;
 | 
				
			||||||
 | 
							snapshot.label = "Файл для теста";
 | 
				
			||||||
 | 
							snapshot.sourceLength = data.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		_db.beginImmediate();
 | 
							_db.beginImmediate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto idSnapshot = _db.addSnapshot(snapshot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							SnapshotChunk snapshotChunk;
 | 
				
			||||||
 | 
							Blob blob;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							blob.zstd = _zstd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Записать фрагменты в БД
 | 
							// Записать фрагменты в БД
 | 
				
			||||||
		foreach (chunk; chunks)
 | 
							foreach (chunk; chunks)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			writeln(format("%(%02x%)", chunk.sha256));
 | 
								blob.sha256 = chunk.sha256;
 | 
				
			||||||
 | 
								blob.size = chunk.size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								auto content = data[chunk.offset .. chunk.offset + chunk.size];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (_zstd) {
 | 
				
			||||||
 | 
									ubyte[] zBytes = compress(content, 22);
 | 
				
			||||||
 | 
									size_t zSize = zBytes.length;
 | 
				
			||||||
 | 
									ubyte[32] zHash = digest!SHA256(zBytes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									blob.zSize = zSize;
 | 
				
			||||||
 | 
									blob.zSha256 = zHash;
 | 
				
			||||||
 | 
									blob.content = zBytes;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									blob.content = content.dup;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_db.addBlob(blob);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								snapshotChunk.snapshotId = idSnapshot;
 | 
				
			||||||
 | 
								snapshotChunk.chunkIndex = chunk.index;
 | 
				
			||||||
 | 
								snapshotChunk.offset = chunk.offset;
 | 
				
			||||||
 | 
								snapshotChunk.size = chunk.size;
 | 
				
			||||||
 | 
								snapshotChunk.sha256 = chunk.sha256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								_db.addSnapshotChunk(snapshotChunk);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		_db.commit();
 | 
							_db.commit();
 | 
				
			||||||
		// Записать манифест в БД
 | 
							// Записать манифест в БД
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,10 @@ module cdcdb.db.dblite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import arsd.sqlite;
 | 
					import arsd.sqlite;
 | 
				
			||||||
import std.file : exists, isFile;
 | 
					import std.file : exists, isFile;
 | 
				
			||||||
 | 
					import std.exception : enforce;
 | 
				
			||||||
 | 
					import std.conv : to;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cdcdb.db.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final class DBLite : Sqlite
 | 
					final class DBLite : Sqlite
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -45,5 +49,235 @@ public:
 | 
				
			||||||
		query("ROLLBACK");
 | 
							query("ROLLBACK");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// findFile()
 | 
						// 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(
 | 
				
			||||||
 | 
								q{
 | 
				
			||||||
 | 
									INSERT INTO snapshots(
 | 
				
			||||||
 | 
										file_path,
 | 
				
			||||||
 | 
										file_sha256,
 | 
				
			||||||
 | 
										label,
 | 
				
			||||||
 | 
										source_length,
 | 
				
			||||||
 | 
										algo_min,
 | 
				
			||||||
 | 
										algo_normal,
 | 
				
			||||||
 | 
										algo_max,
 | 
				
			||||||
 | 
										mask_s,
 | 
				
			||||||
 | 
										mask_l,
 | 
				
			||||||
 | 
										status
 | 
				
			||||||
 | 
									) VALUES (?,?,?,?,?,?,?,?,?,?)
 | 
				
			||||||
 | 
									RETURNING id
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								snapshot.filePath,
 | 
				
			||||||
 | 
								snapshot.fileSha256[],
 | 
				
			||||||
 | 
								snapshot.label,
 | 
				
			||||||
 | 
								snapshot.sourceLength,
 | 
				
			||||||
 | 
								snapshot.algoMin,
 | 
				
			||||||
 | 
								snapshot.algoNormal,
 | 
				
			||||||
 | 
								snapshot.algoMax,
 | 
				
			||||||
 | 
								snapshot.maskS,
 | 
				
			||||||
 | 
								snapshot.maskL,
 | 
				
			||||||
 | 
								snapshot.status.to!int
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!queryResult.empty())
 | 
				
			||||||
 | 
								return queryResult.front()["id"].to!long;
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void addBlob(Blob blob)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							sql(
 | 
				
			||||||
 | 
								q{
 | 
				
			||||||
 | 
									INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd)
 | 
				
			||||||
 | 
									VALUES (?,?,?,?,?,?)
 | 
				
			||||||
 | 
									ON CONFLICT (sha256) DO NOTHING
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								blob.sha256[],
 | 
				
			||||||
 | 
								blob.zSize ? blob.zSha256[] : null,
 | 
				
			||||||
 | 
								blob.size,
 | 
				
			||||||
 | 
								blob.zSize,
 | 
				
			||||||
 | 
								blob.content,
 | 
				
			||||||
 | 
								blob.zstd.to!int
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void addSnapshotChunk(SnapshotChunk snapshotChunk)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							sql(
 | 
				
			||||||
 | 
								q{
 | 
				
			||||||
 | 
									INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, size, sha256)
 | 
				
			||||||
 | 
									VALUES(?,?,?,?,?)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								snapshotChunk.snapshotId,
 | 
				
			||||||
 | 
								snapshotChunk.chunkIndex,
 | 
				
			||||||
 | 
								snapshotChunk.offset,
 | 
				
			||||||
 | 
								snapshotChunk.size,
 | 
				
			||||||
 | 
								snapshotChunk.sha256[]
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// struct ChunkInput
 | 
				
			||||||
 | 
						// {
 | 
				
			||||||
 | 
						// 	long index;
 | 
				
			||||||
 | 
						// 	long offset;
 | 
				
			||||||
 | 
						// 	long size;
 | 
				
			||||||
 | 
						// 	ubyte[32] sha256;
 | 
				
			||||||
 | 
						// 	const(ubyte)[] content;
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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;
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// // --- чтение ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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 s;
 | 
				
			||||||
 | 
						// 	bool found = false;
 | 
				
			||||||
 | 
						// 	foreach (row; queryResult)
 | 
				
			||||||
 | 
						// 	{
 | 
				
			||||||
 | 
						// 		s.id = row[0].to!long;
 | 
				
			||||||
 | 
						// 		s.file_path = row[1].to!string;
 | 
				
			||||||
 | 
						// 		s.label = row[2].to!string;
 | 
				
			||||||
 | 
						// 		s.created_utc = row[3].to!string;
 | 
				
			||||||
 | 
						// 		s.source_length = row[4].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_min = row[5].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_normal = row[6].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_max = row[7].to!long;
 | 
				
			||||||
 | 
						// 		s.mask_s = row[8].to!long;
 | 
				
			||||||
 | 
						// 		s.mask_l = row[9].to!long;
 | 
				
			||||||
 | 
						// 		s.status = cast(SnapshotStatus) row[10].to!int;
 | 
				
			||||||
 | 
						// 		found = true;
 | 
				
			||||||
 | 
						// 		break;
 | 
				
			||||||
 | 
						// 	}
 | 
				
			||||||
 | 
						// 	enforce(found, "getSnapshot: not found");
 | 
				
			||||||
 | 
						// 	return s;
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// SnapshotChunk[] getSnapshotChunks(long snapshotId)
 | 
				
			||||||
 | 
						// {
 | 
				
			||||||
 | 
						// 	auto r = sql(q{
 | 
				
			||||||
 | 
						// 		SELECT snapshot_id,chunk_index,COALESCE(offset,0),size,sha256
 | 
				
			||||||
 | 
						// 		FROM snapshot_chunks
 | 
				
			||||||
 | 
						// 		WHERE snapshot_id=? ORDER BY chunk_index
 | 
				
			||||||
 | 
						// 	}, snapshotId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 	auto acc = appender!SnapshotChunk[];
 | 
				
			||||||
 | 
						// 	foreach (row; r)
 | 
				
			||||||
 | 
						// 	{
 | 
				
			||||||
 | 
						// 		SnapshotChunk ch;
 | 
				
			||||||
 | 
						// 		ch.snapshot_id = row[0].to!long;
 | 
				
			||||||
 | 
						// 		ch.chunk_index = row[1].to!long;
 | 
				
			||||||
 | 
						// 		ch.offset = row[2].to!long;
 | 
				
			||||||
 | 
						// 		ch.size = row[3].to!long;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 		const(ubyte)[] sha = cast(const(ubyte)[]) row[4];
 | 
				
			||||||
 | 
						// 		enforce(sha.length == 32, "getSnapshotChunks: sha256 blob length != 32");
 | 
				
			||||||
 | 
						// 		ch.sha256[] = sha[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 		acc.put(ch);
 | 
				
			||||||
 | 
						// 	}
 | 
				
			||||||
 | 
						// 	return acc.data;
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// /// Вариант без `out`: вернуть Nullable
 | 
				
			||||||
 | 
						// Nullable!Snapshot maybeGetSnapshotByLabel(string label)
 | 
				
			||||||
 | 
						// {
 | 
				
			||||||
 | 
						// 	auto r = sql(q{
 | 
				
			||||||
 | 
						// 		SELECT id,file_path,label,created_utc,source_length,
 | 
				
			||||||
 | 
						// 			algo_min,algo_normal,algo_max,mask_s,mask_l,status
 | 
				
			||||||
 | 
						// 		FROM snapshots
 | 
				
			||||||
 | 
						// 		WHERE label=? ORDER BY id DESC LIMIT 1
 | 
				
			||||||
 | 
						// 	}, label);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 	foreach (row; r)
 | 
				
			||||||
 | 
						// 	{
 | 
				
			||||||
 | 
						// 		Snapshot s;
 | 
				
			||||||
 | 
						// 		s.id = row[0].to!long;
 | 
				
			||||||
 | 
						// 		s.file_path = row[1].to!string;
 | 
				
			||||||
 | 
						// 		s.label = row[2].to!string;
 | 
				
			||||||
 | 
						// 		s.created_utc = row[3].to!string;
 | 
				
			||||||
 | 
						// 		s.source_length = row[4].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_min = row[5].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_normal = row[6].to!long;
 | 
				
			||||||
 | 
						// 		s.algo_max = row[7].to!long;
 | 
				
			||||||
 | 
						// 		s.mask_s = row[8].to!long;
 | 
				
			||||||
 | 
						// 		s.mask_l = row[9].to!long;
 | 
				
			||||||
 | 
						// 		s.status = cast(SnapshotStatus) row[10].to!int;
 | 
				
			||||||
 | 
						// 		return typeof(return)(s); // Nullable!Snapshot(s)
 | 
				
			||||||
 | 
						// 	}
 | 
				
			||||||
 | 
						// 	return typeof(return).init; // null/empty
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// /// Или жёсткий вариант: вернуть/кинуть
 | 
				
			||||||
 | 
						// Snapshot getSnapshotByLabel(string label)
 | 
				
			||||||
 | 
						// {
 | 
				
			||||||
 | 
						// 	auto m = maybeGetSnapshotByLabel(label);
 | 
				
			||||||
 | 
						// 	enforce(!m.isNull, "getSnapshotByLabel: not found");
 | 
				
			||||||
 | 
						// 	return m.get;
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
module cdcdb.db;
 | 
					module cdcdb.db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public import cdcdb.db.dblite;
 | 
					public import cdcdb.db.dblite;
 | 
				
			||||||
 | 
					public import cdcdb.db.types;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@ auto _scheme = [
 | 
				
			||||||
			-- Путь к исходному файлу, для удобства навигации/поиска.
 | 
								-- Путь к исходному файлу, для удобства навигации/поиска.
 | 
				
			||||||
			file_path TEXT,
 | 
								file_path TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								file_sha256 BLOB NOT NULL CHECK (length(file_sha256) = 32),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Произвольная метка/название снимка (для человека).
 | 
								-- Произвольная метка/название снимка (для человека).
 | 
				
			||||||
			label TEXT,
 | 
								label TEXT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,9 +33,10 @@ auto _scheme = [
 | 
				
			||||||
			mask_l INTEGER NOT NULL,
 | 
								mask_l INTEGER NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Состояние снимка:
 | 
								-- Состояние снимка:
 | 
				
			||||||
			-- "pending" - метаданные созданы, состав не полностью загружен;
 | 
								-- 0 - "pending" - метаданные созданы, состав не полностью загружен;
 | 
				
			||||||
			-- "ready"	- все чанки привязаны, снимок готов к использованию.
 | 
								-- 1 - "ready"	- все чанки привязаны, снимок готов к использованию.
 | 
				
			||||||
			status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready"))
 | 
								status INTEGER NOT NULL DEFAULT 0
 | 
				
			||||||
 | 
									CHECK (typeof(status) = "integer" AND status IN (0,1))
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	q{
 | 
						q{
 | 
				
			||||||
| 
						 | 
					@ -41,29 +44,45 @@ auto _scheme = [
 | 
				
			||||||
		-- Уникальные куски содержимого (дедупликация по sha256)
 | 
							-- Уникальные куски содержимого (дедупликация по sha256)
 | 
				
			||||||
		-- ------------------------------------------------------------
 | 
							-- ------------------------------------------------------------
 | 
				
			||||||
		CREATE TABLE IF NOT EXISTS blobs (
 | 
							CREATE TABLE IF NOT EXISTS blobs (
 | 
				
			||||||
			-- Хэш содержимого чанка. Обеспечивает уникальность контента.
 | 
								-- Хэш содержимого чанка. Храним как BLOB(32) (сырые 32 байта SHA-256).
 | 
				
			||||||
			-- Храним как BLOB(32) (сырые 32 байта SHA-256), а не hex-строку.
 | 
					 | 
				
			||||||
			sha256 BLOB PRIMARY KEY CHECK (length(sha256) = 32),
 | 
								sha256 BLOB PRIMARY KEY CHECK (length(sha256) = 32),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Размер чанка в байтах. Должен совпадать с длиной content.
 | 
								-- Хэш сжатого содержимого (если zstd=1). Может быть NULL при zstd=0.
 | 
				
			||||||
 | 
								z_sha256 BLOB,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								-- Размер чанка в байтах (до сжатия).
 | 
				
			||||||
			size INTEGER NOT NULL,
 | 
								size INTEGER NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Сырые байты чанка.
 | 
								-- Размер сжатого чанка (в байтах). Можно держать NOT NULL и заполнять =size при zstd=0,
 | 
				
			||||||
 | 
								-- либо сделать NULL при zstd=0 (см. CHECK ниже допускает NULL).
 | 
				
			||||||
 | 
								z_size INTEGER,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								-- Сырые байты: при zstd=1 здесь лежит сжатый блок, иначе - исходные байты.
 | 
				
			||||||
			content BLOB NOT NULL,
 | 
								content BLOB NOT NULL,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Момент, когда этот контент впервые появился в базе (UTC).
 | 
								-- Таймштампы
 | 
				
			||||||
			created_utc	TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
								created_utc   TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
				
			||||||
 | 
					 | 
				
			||||||
			-- Последний раз, когда на контент сослались (для аналитики/GC).
 | 
					 | 
				
			||||||
			last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
								last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Счётчик ссылок: сколько строк в snapshot_chunks ссылаются на этот sha256.
 | 
								-- Счётчик ссылок из snapshot_chunks
 | 
				
			||||||
			-- Используется для безопасного удаления неиспользуемых blob-ов.
 | 
					 | 
				
			||||||
			refcount INTEGER NOT NULL DEFAULT 0,
 | 
								refcount INTEGER NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								-- Флаг сжатия: 0 - без сжатия; 1 - zstd
 | 
				
			||||||
 | 
								zstd INTEGER NOT NULL DEFAULT 0
 | 
				
			||||||
 | 
									CHECK (typeof(zstd) = "integer" AND zstd IN (0,1)),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			-- Дополнительные гарантии целостности:
 | 
								-- Дополнительные гарантии целостности:
 | 
				
			||||||
			CHECK (refcount >= 0),
 | 
								CHECK (refcount >= 0),
 | 
				
			||||||
			CHECK (size = length(content))
 | 
					
 | 
				
			||||||
 | 
								-- Если zstd=1, длина content должна равняться z_size;
 | 
				
			||||||
 | 
								-- если zstd=0 - длина content должна равняться size.
 | 
				
			||||||
 | 
								CHECK (
 | 
				
			||||||
 | 
									(zstd = 1 AND z_size IS NOT NULL AND length(content) = z_size)
 | 
				
			||||||
 | 
								OR (zstd = 0 AND length(content) = size )
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								-- Согласованность z_sha256 (если задан)
 | 
				
			||||||
 | 
								CHECK (z_sha256 IS NULL OR length(z_sha256) = 32)
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	q{
 | 
						q{
 | 
				
			||||||
| 
						 | 
					@ -106,6 +125,10 @@ auto _scheme = [
 | 
				
			||||||
				ON DELETE RESTRICT
 | 
									ON DELETE RESTRICT
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
						q{
 | 
				
			||||||
 | 
							CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
 | 
				
			||||||
 | 
								ON snapshots(file_path, file_sha256)
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
	q{
 | 
						q{
 | 
				
			||||||
		-- Быстрый выбор всех чанков конкретного снимка (частый запрос).
 | 
							-- Быстрый выбор всех чанков конкретного снимка (частый запрос).
 | 
				
			||||||
		CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot
 | 
							CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot
 | 
				
			||||||
| 
						 | 
					@ -172,15 +195,15 @@ auto _scheme = [
 | 
				
			||||||
		END
 | 
							END
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	q{
 | 
						q{
 | 
				
			||||||
		-- Автоматическая смена статуса снимка на "ready",
 | 
							-- Автоматическая смена статуса снимка на 1 - "ready",
 | 
				
			||||||
		-- когда сумма размеров его чанков стала равна source_length.
 | 
							-- когда сумма размеров его чанков стала равна source_length.
 | 
				
			||||||
		-- Примечание: простая эвристика; если потом удалишь/поменяешь чанки,
 | 
							-- Примечание: простая эвристика; если потом удалишь/поменяешь чанки,
 | 
				
			||||||
		-- триггер ниже вернёт статус обратно на "pending".
 | 
							-- триггер ниже вернёт статус обратно на 0 - "pending".
 | 
				
			||||||
		CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_ready
 | 
							CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_ready
 | 
				
			||||||
		AFTER INSERT ON snapshot_chunks
 | 
							AFTER INSERT ON snapshot_chunks
 | 
				
			||||||
		BEGIN
 | 
							BEGIN
 | 
				
			||||||
		UPDATE snapshots
 | 
							UPDATE snapshots
 | 
				
			||||||
			SET status = "ready"
 | 
								SET status = 1
 | 
				
			||||||
		WHERE id = NEW.snapshot_id
 | 
							WHERE id = NEW.snapshot_id
 | 
				
			||||||
			AND (SELECT COALESCE(SUM(size),0)
 | 
								AND (SELECT COALESCE(SUM(size),0)
 | 
				
			||||||
					FROM snapshot_chunks
 | 
										FROM snapshot_chunks
 | 
				
			||||||
| 
						 | 
					@ -189,13 +212,13 @@ auto _scheme = [
 | 
				
			||||||
		END
 | 
							END
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	q{
 | 
						q{
 | 
				
			||||||
		-- При удалении любого чанка снимок снова помечается как "pending".
 | 
							-- При удалении любого чанка снимок снова помечается как 0 - "pending".
 | 
				
			||||||
		-- Это простой безопасный фоллбэк; следующая вставка приравняет суммы и вернёт "ready".
 | 
							-- Это простой безопасный фоллбэк; следующая вставка приравняет суммы и вернёт 1 - "ready".
 | 
				
			||||||
		CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_pending
 | 
							CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_pending
 | 
				
			||||||
		AFTER DELETE ON snapshot_chunks
 | 
							AFTER DELETE ON snapshot_chunks
 | 
				
			||||||
		BEGIN
 | 
							BEGIN
 | 
				
			||||||
		UPDATE snapshots
 | 
							UPDATE snapshots
 | 
				
			||||||
			SET status = "pending"
 | 
								SET status = 0
 | 
				
			||||||
		WHERE id = OLD.snapshot_id;
 | 
							WHERE id = OLD.snapshot_id;
 | 
				
			||||||
		END
 | 
							END
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -149,3 +149,49 @@ sequenceDiagram
 | 
				
			||||||
        APP-->>APP: Фиксирует ошибку (несоответствие сумм)
 | 
					        APP-->>APP: Фиксирует ошибку (несоответствие сумм)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Схема записи в БД
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```mermaid
 | 
				
			||||||
 | 
					sequenceDiagram
 | 
				
			||||||
 | 
					  autonumber
 | 
				
			||||||
 | 
					  participant APP as Приложение
 | 
				
			||||||
 | 
					  participant DB as SQLite
 | 
				
			||||||
 | 
					  participant CH as Разбиение (FastCDC)
 | 
				
			||||||
 | 
					  participant HS as SHA-256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Note over APP,DB: Подготовка к записи
 | 
				
			||||||
 | 
					  APP->>DB: PRAGMA foreign_keys=ON
 | 
				
			||||||
 | 
					  APP->>DB: BEGIN IMMEDIATE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Note over APP,DB: Метаданные снимка
 | 
				
			||||||
 | 
					  APP->>DB: INSERT INTO snapshots(..., status='pending')
 | 
				
			||||||
 | 
					  DB-->>APP: snap_id := last_insert_rowid()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Note over APP,CH: Поток файла → чанки (min/normal/max, mask_s/mask_l)
 | 
				
			||||||
 | 
					  loop Для каждого чанка по порядку
 | 
				
			||||||
 | 
					    CH-->>APP: {chunk_index, offset, size, bytes}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note over APP,HS: Хеширование
 | 
				
			||||||
 | 
					    APP->>HS: SHA-256(bytes)
 | 
				
			||||||
 | 
					    HS-->>APP: sha256 (32 байта)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note over APP,DB: Дедупликация содержимого
 | 
				
			||||||
 | 
					    APP->>DB: INSERT INTO blobs(sha256,size,content) ON CONFLICT DO NOTHING
 | 
				
			||||||
 | 
					    DB-->>APP: OK (новая строка или уже была)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Note over APP,DB: Привязка к снимку
 | 
				
			||||||
 | 
					    APP->>DB: INSERT INTO snapshot_chunks(snapshot_id,chunk_index,offset,size,sha256)
 | 
				
			||||||
 | 
					    DB-->>APP: OK (триггер ++refcount, last_seen_utc=now)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Note over APP,DB: Валидация и финал
 | 
				
			||||||
 | 
					  APP->>DB: SELECT SUM(size) FROM snapshot_chunks WHERE snapshot_id = snap_id
 | 
				
			||||||
 | 
					  DB-->>APP: total_size
 | 
				
			||||||
 | 
					  alt total_size == snapshots.source_length
 | 
				
			||||||
 | 
					    Note over DB: триггер mark_ready ставит status='ready'
 | 
				
			||||||
 | 
					    APP->>DB: COMMIT
 | 
				
			||||||
 | 
					  else несовпадение / ошибка
 | 
				
			||||||
 | 
					    APP->>DB: ROLLBACK
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										45
									
								
								source/cdcdb/db/types.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								source/cdcdb/db/types.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					module cdcdb.db.types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SnapshotStatus : int
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						pending = 0,
 | 
				
			||||||
 | 
						ready = 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Snapshot
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						long id;
 | 
				
			||||||
 | 
						string filePath;
 | 
				
			||||||
 | 
						ubyte[32] fileSha256;
 | 
				
			||||||
 | 
						string label;
 | 
				
			||||||
 | 
						string createdUtc;
 | 
				
			||||||
 | 
						long sourceLength;
 | 
				
			||||||
 | 
						long algoMin;
 | 
				
			||||||
 | 
						long algoNormal;
 | 
				
			||||||
 | 
						long algoMax;
 | 
				
			||||||
 | 
						long maskS;
 | 
				
			||||||
 | 
						long maskL;
 | 
				
			||||||
 | 
						SnapshotStatus status;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Blob
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						ubyte[32] sha256; // BLOB(32)
 | 
				
			||||||
 | 
						ubyte[32] zSha256; // BLOB(32)
 | 
				
			||||||
 | 
						long size;
 | 
				
			||||||
 | 
						long zSize;
 | 
				
			||||||
 | 
						ubyte[] content; // BLOB
 | 
				
			||||||
 | 
						string createdUtc;
 | 
				
			||||||
 | 
						string lastSeenUtc;
 | 
				
			||||||
 | 
						long refcount;
 | 
				
			||||||
 | 
						bool zstd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct SnapshotChunk
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						long snapshotId;
 | 
				
			||||||
 | 
						long chunkIndex;
 | 
				
			||||||
 | 
						long offset;
 | 
				
			||||||
 | 
						long size;
 | 
				
			||||||
 | 
						ubyte[32] sha256; // BLOB(32)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,6 @@ import std.file : read;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main()
 | 
					void main()
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	auto cas = new CAS("/tmp/base.db");
 | 
						auto cas = new CAS("/tmp/base.db", true);
 | 
				
			||||||
	cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
						cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue