forked from dlang/cdcdb
		
	Compare commits
	
		
			3 commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 578e6a3358 | |||
| 0fc56e7c04 | |||
| 1f50b21457 | 
					 14 changed files with 1011 additions and 592 deletions
				
			
		
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -15,4 +15,4 @@ cdcdb-test-*
 | 
			
		|||
*.obj
 | 
			
		||||
*.lst
 | 
			
		||||
bin
 | 
			
		||||
lib
 | 
			
		||||
/lib
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								dub.json
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								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": [
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
	"fileVersion": 1,
 | 
			
		||||
	"versions": {
 | 
			
		||||
		"arsd-official": "12.0.0",
 | 
			
		||||
		"d2sqlite3": "1.0.0",
 | 
			
		||||
		"zstd": "0.2.1"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										110
									
								
								source/cdcdb/lib/hash.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								source/cdcdb/lib/hash.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,110 @@
 | 
			
		|||
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(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);
 | 
			
		||||
		_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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								source/cdcdb/lib/package.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								source/cdcdb/lib/package.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
module cdcdb.lib;
 | 
			
		||||
 | 
			
		||||
public import cdcdb.lib.hash;
 | 
			
		||||
public import cdcdb.lib.uts;
 | 
			
		||||
							
								
								
									
										61
									
								
								source/cdcdb/lib/uts.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								source/cdcdb/lib/uts.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,6 @@
 | 
			
		|||
module cdcdb;
 | 
			
		||||
 | 
			
		||||
public import cdcdb.lib;
 | 
			
		||||
public import cdcdb.storage;
 | 
			
		||||
public import cdcdb.storagefile;
 | 
			
		||||
public import cdcdb.snapshot;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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; }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										25
									
								
								source/cdcdb/storagefile.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								source/cdcdb/storagefile.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,3 @@
 | 
			
		|||
module cdcdb.version_;
 | 
			
		||||
 | 
			
		||||
enum cdcdbVersion = "0.1.0";
 | 
			
		||||
enum cdcdbVersion = "0.2.0";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								test/app.d
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								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");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue