forked from dlang/cdcdb
		
	Обновлена схема БД - добавлены таблицы users, processes, files. Добавлен параметр Context с идентификацией пользователей и процесса. label сменен на file.
This commit is contained in:
		
							parent
							
								
									5bb4d65c92
								
							
						
					
					
						commit
						49ee7a4053
					
				
					 7 changed files with 289 additions and 168 deletions
				
			
		| 
						 | 
				
			
			@ -1,12 +0,0 @@
 | 
			
		|||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [0.1.0] — 2025-09-13
 | 
			
		||||
### Added
 | 
			
		||||
- Библиотека для снимков данных на базе SQLite с контентно-зависимым разбиением (FastCDC).
 | 
			
		||||
- Дедупликация по SHA-256 чанков, опциональная компрессия Zstd.
 | 
			
		||||
- Сквозная проверка целостности: хеш каждого чанка и итогового файла.
 | 
			
		||||
- Транзакции (WAL), базовые ограничения целостности и триггеры.
 | 
			
		||||
- Высокоуровневый API:
 | 
			
		||||
  - `Storage`: `newSnapshot`, `getSnapshots`, `getSnapshot`, `removeSnapshots`, `setupCDC`, `getVersion`.
 | 
			
		||||
  - `Snapshot`: `data()` (буфер) и потоковый `data(void delegate(const(ubyte)[]))`, `remove()`, свойства (`id`, `label`, `created`, `length`, `sha256`, `status`, `description`).
 | 
			
		||||
- Инструмент для генерации Gear-таблицы для FastCDC (`tools/gen.d`).
 | 
			
		||||
							
								
								
									
										98
									
								
								README.ru.md
									
										
									
									
									
								
							
							
						
						
									
										98
									
								
								README.ru.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,98 +0,0 @@
 | 
			
		|||
# cdcdb
 | 
			
		||||
 | 
			
		||||
Библиотека для хранения и управления снимками текстовых данных в базе SQLite. Использует механизм content-defined chunking (CDC) на основе алгоритма FastCDC для разделения данных на чанки переменного размера, что обеспечивает эффективную дедупликацию. Поддерживает опциональную компрессию Zstd, транзакции и проверку целостности данных через SHA-256. Основное применение — резервное копирование и версионирование текстовых файлов с минимизацией занимаемого пространства.
 | 
			
		||||
 | 
			
		||||
## Алгоритм FastCDC
 | 
			
		||||
FastCDC — это алгоритм разделения данных на чанки переменного размера, основанный на хэшировании содержимого. Он использует таблицу Gear для вычисления "отпечатков" данных, определяя точки разделения с учетом минимального, целевого и максимального размеров чанка. Это позволяет эффективно выявлять изменения в данных и хранить только уникальные чанки, снижая объем хранилища.
 | 
			
		||||
 | 
			
		||||
## Основные классы
 | 
			
		||||
 | 
			
		||||
### Storage
 | 
			
		||||
Класс для работы с базой SQLite и управления снимками.
 | 
			
		||||
 | 
			
		||||
- **Конструктор**: Инициализирует подключение к базе SQLite.
 | 
			
		||||
- **Методы**:
 | 
			
		||||
  - `newSnapshot`: Создает снимок данных. Возвращает объект `Snapshot` или `null`, если данные совпадают с последним снимком.
 | 
			
		||||
  - `getSnapshots`: Получает список снимков (все или по метке). Возвращает массив объектов `Snapshot`.
 | 
			
		||||
  - `getSnapshot`: Получает снимок по ID. Возвращает объект `Snapshot`.
 | 
			
		||||
  - `setupCDC`: Настраивает параметры разделения данных CDC. Ничего не возвращает.
 | 
			
		||||
  - `removeSnapshots`: Удаляет снимки по метке, ID или объекту Snapshot. Возвращает количество удаленных снимков (для метки) или `true`/`false` (для ID или объекта).
 | 
			
		||||
  - `getVersion`: Возвращает строку с версией библиотеки.
 | 
			
		||||
 | 
			
		||||
### Snapshot
 | 
			
		||||
Класс для работы с отдельным снимком данных.
 | 
			
		||||
 | 
			
		||||
- **Конструктор**: Создает объект снимка для работы с данными по его ID.
 | 
			
		||||
- **Методы**:
 | 
			
		||||
  - `data`: Извлекает полные данные снимка. Возвращает массив байтов (`ubyte[]`).
 | 
			
		||||
  - `data`: Извлекает данные снимка потоково через делегат. Ничего не возвращает.
 | 
			
		||||
  - `remove`: Удаляет снимок из базы. Возвращает `true` при успешном удалении, иначе `false`.
 | 
			
		||||
 | 
			
		||||
- **Свойства**:
 | 
			
		||||
  - `id`: ID снимка (long).
 | 
			
		||||
  - `label`: Метка снимка (string).
 | 
			
		||||
  - `created`: Временная метка создания (UTC, `DateTime`).
 | 
			
		||||
  - `length`: Длина исходных данных (long).
 | 
			
		||||
  - `sha256`: Хэш SHA-256 данных (ubyte[32]).
 | 
			
		||||
  - `status`: Статус снимка ("pending" или "ready").
 | 
			
		||||
  - `description`: Описание снимка (string).
 | 
			
		||||
 | 
			
		||||
## Пример использования
 | 
			
		||||
```d
 | 
			
		||||
import cdcdb;
 | 
			
		||||
 | 
			
		||||
import std.stdio : writeln, File;
 | 
			
		||||
import std.file : exists, remove;
 | 
			
		||||
 | 
			
		||||
void main()
 | 
			
		||||
{
 | 
			
		||||
	// Создаем базу
 | 
			
		||||
	string dbPath = "example.db";
 | 
			
		||||
 | 
			
		||||
	// Инициализация Storage с компрессией Zstd
 | 
			
		||||
	auto storage = new Storage(dbPath, true, 22);
 | 
			
		||||
 | 
			
		||||
	// Создание снимка
 | 
			
		||||
	ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
 | 
			
		||||
	auto snap = storage.newSnapshot("example_file", data, "Версия 1.0");
 | 
			
		||||
	if (snap)
 | 
			
		||||
	{
 | 
			
		||||
		writeln("Создан снимок: ID=", snap.id, ", Метка=", snap.label);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Восстановление данных
 | 
			
		||||
	auto snapshots = storage.getSnapshots("example_file");
 | 
			
		||||
	if (snapshots.length > 0)
 | 
			
		||||
	{
 | 
			
		||||
		auto lastSnap = snapshots[0];
 | 
			
		||||
		File outFile = File("restored.txt", "wb");
 | 
			
		||||
		lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
 | 
			
		||||
		outFile.close();
 | 
			
		||||
		writeln("Данные восстановлены в restored.txt");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Удаление снимков
 | 
			
		||||
	long deleted = storage.removeSnapshots("example_file");
 | 
			
		||||
	writeln("Удалено снимков: ", deleted);
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Инструменты
 | 
			
		||||
В директории `tools` находится скрипт на D для создания таблицы Gear, используемой в алгоритме FastCDC. Он позволяет генерировать пользовательские таблицы хэшей для настройки разделения данных. Для создания новой таблицы выполните:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
chmod +x ./tools/gen.d
 | 
			
		||||
./tools/gen.d > ./source/gear.d
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Установка
 | 
			
		||||
- **В `dub.json`**:
 | 
			
		||||
	```json
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"cdcdb": "~>0.1.0"
 | 
			
		||||
	}
 | 
			
		||||
	```
 | 
			
		||||
- **Сборка**: `dub build`.
 | 
			
		||||
 | 
			
		||||
## Лицензия
 | 
			
		||||
Boost Software License 1.0 (BSL-1.0).
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ enum SnapshotStatus : ubyte
 | 
			
		|||
 | 
			
		||||
struct DBSnapshot {
 | 
			
		||||
	long id;
 | 
			
		||||
	string label;
 | 
			
		||||
	string file;
 | 
			
		||||
	ubyte[32] sha256;
 | 
			
		||||
	string description;
 | 
			
		||||
	DateTime createdUtc;
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +28,11 @@ struct DBSnapshot {
 | 
			
		|||
	long maskS;
 | 
			
		||||
	long maskL;
 | 
			
		||||
	SnapshotStatus status;
 | 
			
		||||
	long uid;
 | 
			
		||||
	long ruid;
 | 
			
		||||
	string uidName;
 | 
			
		||||
	string ruidName;
 | 
			
		||||
	string process;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct DBSnapshotChunk
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +109,8 @@ private:
 | 
			
		|||
	{
 | 
			
		||||
		SqliteResult queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"))
 | 
			
		||||
				WITH required(name)
 | 
			
		||||
					AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files"))
 | 
			
		||||
				SELECT name AS missing_table
 | 
			
		||||
				FROM required
 | 
			
		||||
				WHERE NOT EXISTS (
 | 
			
		||||
| 
						 | 
				
			
			@ -122,11 +128,11 @@ private:
 | 
			
		|||
			missingTables ~= row["missing_table"].to!string;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		enforce(missingTables.length == 0 || missingTables.length == 3,
 | 
			
		||||
		enforce(missingTables.length == 0 || missingTables.length == 6,
 | 
			
		||||
			"Database is corrupted. Missing tables: " ~ missingTables.join(", ")
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (missingTables.length == 3)
 | 
			
		||||
		if (missingTables.length == 6)
 | 
			
		||||
		{
 | 
			
		||||
			foreach (schemeQuery; _scheme)
 | 
			
		||||
			{
 | 
			
		||||
| 
						 | 
				
			
			@ -172,17 +178,21 @@ public:
 | 
			
		|||
		sql("ROLLBACK");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool isLast(string label, ubyte[] sha256) {
 | 
			
		||||
	bool isLast(string file, ubyte[] sha256) {
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT COALESCE(
 | 
			
		||||
					(SELECT (label = ? AND sha256 = ?)
 | 
			
		||||
					FROM snapshots
 | 
			
		||||
					ORDER BY created_utc DESC
 | 
			
		||||
					LIMIT 1),
 | 
			
		||||
					(
 | 
			
		||||
						SELECT (s.sha256 = ?2)
 | 
			
		||||
						FROM snapshots s
 | 
			
		||||
						JOIN files f ON f.id = s.file
 | 
			
		||||
						WHERE f.name = ?1
 | 
			
		||||
						ORDER BY s.created_utc DESC
 | 
			
		||||
						LIMIT 1
 | 
			
		||||
					),
 | 
			
		||||
					0
 | 
			
		||||
				) AS is_last;
 | 
			
		||||
			}, label, sha256
 | 
			
		||||
			}, file, sha256
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		if (!queryResult.empty())
 | 
			
		||||
| 
						 | 
				
			
			@ -195,23 +205,34 @@ public:
 | 
			
		|||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				INSERT INTO snapshots(
 | 
			
		||||
					label,
 | 
			
		||||
					file,
 | 
			
		||||
					sha256,
 | 
			
		||||
					description,
 | 
			
		||||
					source_length,
 | 
			
		||||
					uid,
 | 
			
		||||
					ruid,
 | 
			
		||||
					process,
 | 
			
		||||
					algo_min,
 | 
			
		||||
					algo_normal,
 | 
			
		||||
					algo_max,
 | 
			
		||||
					mask_s,
 | 
			
		||||
					mask_l,
 | 
			
		||||
					status
 | 
			
		||||
				) VALUES (?,?,?,?,?,?,?,?,?,?)
 | 
			
		||||
				)
 | 
			
		||||
				SELECT
 | 
			
		||||
					(SELECT id FROM files WHERE name = ?),
 | 
			
		||||
					?,?,?,?,?,
 | 
			
		||||
					(SELECT id FROM processes WHERE name = ?),
 | 
			
		||||
					?,?,?,?,?,?
 | 
			
		||||
				RETURNING id
 | 
			
		||||
			},
 | 
			
		||||
			snapshot.label,
 | 
			
		||||
			snapshot.file,
 | 
			
		||||
			snapshot.sha256[],
 | 
			
		||||
			snapshot.description.length ? snapshot.description : null,
 | 
			
		||||
			snapshot.sourceLength,
 | 
			
		||||
			snapshot.uid,
 | 
			
		||||
			snapshot.ruid,
 | 
			
		||||
			snapshot.process,
 | 
			
		||||
			snapshot.algoMin,
 | 
			
		||||
			snapshot.algoNormal,
 | 
			
		||||
			snapshot.algoMax,
 | 
			
		||||
| 
						 | 
				
			
			@ -247,6 +268,43 @@ public:
 | 
			
		|||
		return !queryResult.empty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool addProcess(string name)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				INSERT INTO processes (name) VALUES (?)
 | 
			
		||||
				ON CONFLICT(name) DO NOTHING
 | 
			
		||||
			}, name
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		return !queryResult.empty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool addFile(string name)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				INSERT INTO files (name) VALUES (?)
 | 
			
		||||
				ON CONFLICT(name) DO NOTHING
 | 
			
		||||
			}, name
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		return !queryResult.empty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool addUser(long uid, string name)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				INSERT INTO users (uid, name)
 | 
			
		||||
				VALUES (?, ?)
 | 
			
		||||
				ON CONFLICT(uid) DO NOTHING;
 | 
			
		||||
			}, uid, name
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		return !queryResult.empty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
| 
						 | 
				
			
			@ -268,9 +326,30 @@ public:
 | 
			
		|||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT id, label, sha256, description, created_utc, source_length,
 | 
			
		||||
					algo_min, algo_normal, algo_max, mask_s, mask_l, status
 | 
			
		||||
				FROM snapshots WHERE id = ?
 | 
			
		||||
				SELECT
 | 
			
		||||
					s.id,
 | 
			
		||||
					f.name file,
 | 
			
		||||
					s.sha256,
 | 
			
		||||
					s.description,
 | 
			
		||||
					s.created_utc,
 | 
			
		||||
					s.source_length,
 | 
			
		||||
					s.uid,
 | 
			
		||||
					s.ruid,
 | 
			
		||||
					u.name uid_name,
 | 
			
		||||
					r.name ruid_name,
 | 
			
		||||
					p.name process,
 | 
			
		||||
					s.algo_min,
 | 
			
		||||
					s.algo_normal,
 | 
			
		||||
					s.algo_max,
 | 
			
		||||
					s.mask_s,
 | 
			
		||||
					s.mask_l,
 | 
			
		||||
					s.status
 | 
			
		||||
				FROM snapshots s
 | 
			
		||||
				JOIN processes p ON p.id = s.process
 | 
			
		||||
				JOIN users u ON u.uid = s.uid
 | 
			
		||||
				JOIN users r ON r.uid = s.ruid
 | 
			
		||||
				JOIN files f ON f.id = s.file
 | 
			
		||||
				WHERE s.id = ?;
 | 
			
		||||
			}, id
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -281,7 +360,7 @@ public:
 | 
			
		|||
			auto data = queryResult.front();
 | 
			
		||||
 | 
			
		||||
			snapshot.id = data["id"].to!long;
 | 
			
		||||
			snapshot.label = data["label"].to!string;
 | 
			
		||||
			snapshot.file = data["file"].to!string;
 | 
			
		||||
			snapshot.sha256 = cast(ubyte[]) data["sha256"].dup;
 | 
			
		||||
			snapshot.description = data["description"].to!string;
 | 
			
		||||
			snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
 | 
			
		||||
| 
						 | 
				
			
			@ -292,19 +371,45 @@ public:
 | 
			
		|||
			snapshot.maskS = data["mask_s"].to!long;
 | 
			
		||||
			snapshot.maskL = data["mask_l"].to!long;
 | 
			
		||||
			snapshot.status = cast(SnapshotStatus) data["status"].to!int;
 | 
			
		||||
			snapshot.uid = data["uid"].to!long;
 | 
			
		||||
			snapshot.ruid = data["ruid"].to!long;
 | 
			
		||||
			snapshot.uidName = data["uid_name"].to!string;
 | 
			
		||||
			snapshot.ruidName = data["ruid_name"].to!string;
 | 
			
		||||
			snapshot.process = data["process"].to!string;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return snapshot;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	DBSnapshot[] getSnapshots(string label)
 | 
			
		||||
	DBSnapshot[] getSnapshots(string file)
 | 
			
		||||
	{
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				SELECT id, label, sha256, description, created_utc, source_length,
 | 
			
		||||
					algo_min, algo_normal, algo_max, mask_s, mask_l, status
 | 
			
		||||
				FROM snapshots WHERE (length(?) = 0 OR label = ?1);
 | 
			
		||||
			}, label
 | 
			
		||||
				SELECT
 | 
			
		||||
					s.id,
 | 
			
		||||
					f.name file,
 | 
			
		||||
					s.sha256,
 | 
			
		||||
					s.description,
 | 
			
		||||
					s.created_utc,
 | 
			
		||||
					s.source_length,
 | 
			
		||||
					s.uid,
 | 
			
		||||
					s.ruid,
 | 
			
		||||
					u.name uid_name,
 | 
			
		||||
					r.name ruid_name,
 | 
			
		||||
					p.name process,
 | 
			
		||||
					s.algo_min,
 | 
			
		||||
					s.algo_normal,
 | 
			
		||||
					s.algo_max,
 | 
			
		||||
					s.mask_s,
 | 
			
		||||
					s.mask_l,
 | 
			
		||||
					s.status
 | 
			
		||||
				FROM snapshots s
 | 
			
		||||
				JOIN processes p ON p.id = s.process
 | 
			
		||||
				JOIN users u ON u.uid = s.uid
 | 
			
		||||
				JOIN users r ON r.uid = s.ruid
 | 
			
		||||
				JOIN files f ON f.id = s.file AND (length(?) = 0 OR f.name = ?1)
 | 
			
		||||
				ORDER BY s.created_utc, s.id;
 | 
			
		||||
			}, file
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		DBSnapshot[] snapshots;
 | 
			
		||||
| 
						 | 
				
			
			@ -314,7 +419,7 @@ public:
 | 
			
		|||
			DBSnapshot snapshot;
 | 
			
		||||
 | 
			
		||||
			snapshot.id = row["id"].to!long;
 | 
			
		||||
			snapshot.label = row["label"].to!string;
 | 
			
		||||
			snapshot.file = row["file"].to!string;
 | 
			
		||||
			snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
 | 
			
		||||
			snapshot.description = row["description"].to!string;
 | 
			
		||||
			snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
 | 
			
		||||
| 
						 | 
				
			
			@ -325,6 +430,11 @@ public:
 | 
			
		|||
			snapshot.maskS = row["mask_s"].to!long;
 | 
			
		||||
			snapshot.maskL = row["mask_l"].to!long;
 | 
			
		||||
			snapshot.status = cast(SnapshotStatus) row["status"].to!int;
 | 
			
		||||
			snapshot.uid = row["uid"].to!long;
 | 
			
		||||
			snapshot.ruid = row["ruid"].to!long;
 | 
			
		||||
			snapshot.uidName = row["uid_name"].to!string;
 | 
			
		||||
			snapshot.ruidName = row["ruid_name"].to!string;
 | 
			
		||||
			snapshot.process = row["process"].to!string;
 | 
			
		||||
 | 
			
		||||
			snapshots ~= snapshot;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -375,13 +485,13 @@ public:
 | 
			
		|||
		return 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	long deleteSnapshot(string label) {
 | 
			
		||||
	long deleteSnapshots(string file) {
 | 
			
		||||
		auto queryResult = sql(
 | 
			
		||||
			q{
 | 
			
		||||
				DELETE FROM snapshots
 | 
			
		||||
				WHERE label = ?
 | 
			
		||||
				WHERE file = (SELECT id FROM files WHERE name = ?)
 | 
			
		||||
				RETURNING 1;
 | 
			
		||||
			}, label);
 | 
			
		||||
			}, file);
 | 
			
		||||
 | 
			
		||||
		if (!queryResult.empty()) {
 | 
			
		||||
			return queryResult.length.to!long;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,52 @@
 | 
			
		|||
auto _scheme = [
 | 
			
		||||
	q{
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		-- Таблица users
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS users (
 | 
			
		||||
			-- Linux UID
 | 
			
		||||
			uid INTEGER PRIMARY KEY,
 | 
			
		||||
			-- текстовое представление пользователя
 | 
			
		||||
			name TEXT NOT NULL UNIQUE
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс по имени пользователя
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_users_name
 | 
			
		||||
			ON users(name)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		-- Таблица processes
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS processes (
 | 
			
		||||
			-- идентификатор процесса
 | 
			
		||||
			id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
			-- имя процесса
 | 
			
		||||
			name TEXT NOT NULL UNIQUE
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс по имени процесса
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_processes_name
 | 
			
		||||
			ON processes(name)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		-- Таблица files
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS files (
 | 
			
		||||
			-- идентификатор процесса
 | 
			
		||||
			id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
			-- имя процесса
 | 
			
		||||
			name TEXT NOT NULL UNIQUE
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс по имени процесса
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_processes_name
 | 
			
		||||
			ON processes(name)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		-- Таблица snapshots
 | 
			
		||||
| 
						 | 
				
			
			@ -6,8 +54,8 @@ auto _scheme = [
 | 
			
		|||
		CREATE TABLE IF NOT EXISTS snapshots (
 | 
			
		||||
			-- идентификатор снимка
 | 
			
		||||
			id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
			-- метка/название снимка
 | 
			
		||||
			label TEXT NOT NULL,
 | 
			
		||||
			-- Файл
 | 
			
		||||
			file INTEGER NOT NULL,
 | 
			
		||||
			-- SHA-256 всего файла (BLOB(32))
 | 
			
		||||
			sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
 | 
			
		||||
			-- Комментарий/описание
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +64,12 @@ auto _scheme = [
 | 
			
		|||
			created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
 | 
			
		||||
			-- длина исходного файла в байтах
 | 
			
		||||
			source_length INTEGER NOT NULL,
 | 
			
		||||
			-- UID пользователя (эффективный)
 | 
			
		||||
			uid INTEGER NOT NULL,
 | 
			
		||||
			-- RUID пользователя (реальный)
 | 
			
		||||
			ruid INTEGER NOT NULL,
 | 
			
		||||
			-- Процесс
 | 
			
		||||
			process INTEGER NOT NULL,
 | 
			
		||||
			-- FastCDC: минимальный размер чанка
 | 
			
		||||
			algo_min INTEGER NOT NULL,
 | 
			
		||||
			-- FastCDC: целевой размер чанка
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +82,24 @@ auto _scheme = [
 | 
			
		|||
			mask_l INTEGER NOT NULL,
 | 
			
		||||
			-- 0=pending, 1=ready
 | 
			
		||||
			status INTEGER NOT NULL DEFAULT 0
 | 
			
		||||
				CHECK (status IN (0,1))
 | 
			
		||||
				CHECK (status IN (0,1)),
 | 
			
		||||
			-- Внешние ключи
 | 
			
		||||
			FOREIGN KEY (uid)
 | 
			
		||||
				REFERENCES users(uid)
 | 
			
		||||
				ON UPDATE RESTRICT
 | 
			
		||||
				ON DELETE RESTRICT,
 | 
			
		||||
			FOREIGN KEY (ruid)
 | 
			
		||||
				REFERENCES users(uid)
 | 
			
		||||
				ON UPDATE RESTRICT
 | 
			
		||||
				ON DELETE RESTRICT,
 | 
			
		||||
			FOREIGN KEY (process)
 | 
			
		||||
				REFERENCES processes(id)
 | 
			
		||||
				ON UPDATE RESTRICT
 | 
			
		||||
				ON DELETE RESTRICT
 | 
			
		||||
			FOREIGN KEY (file)
 | 
			
		||||
				REFERENCES files(id)
 | 
			
		||||
				ON UPDATE CASCADE
 | 
			
		||||
				ON DELETE CASCADE
 | 
			
		||||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
| 
						 | 
				
			
			@ -88,9 +159,9 @@ auto _scheme = [
 | 
			
		|||
		)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс для запросов вида: WHERE label=? AND sha256=?
 | 
			
		||||
		-- Индекс для запросов вида: WHERE file=? AND sha256=?
 | 
			
		||||
		CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
 | 
			
		||||
			ON snapshots(label, sha256)
 | 
			
		||||
			ON snapshots(file, sha256)
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- Индекс для обратного поиска использования blob по sha256
 | 
			
		||||
| 
						 | 
				
			
			@ -215,11 +286,14 @@ auto _scheme = [
 | 
			
		|||
		FOR EACH ROW
 | 
			
		||||
		WHEN
 | 
			
		||||
			NEW.id IS NOT OLD.id OR
 | 
			
		||||
			NEW.label IS NOT OLD.label OR
 | 
			
		||||
			NEW.file IS NOT OLD.file 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.uid IS NOT OLD.uid OR
 | 
			
		||||
			NEW.ruid IS NOT OLD.ruid OR
 | 
			
		||||
			NEW.process IS NOT OLD.process 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
 | 
			
		||||
| 
						 | 
				
			
			@ -249,5 +323,20 @@ auto _scheme = [
 | 
			
		|||
		BEGIN
 | 
			
		||||
			SELECT RAISE(ABORT, "blobs: разрешён UPDATE только полей last_seen_utc и refcount");
 | 
			
		||||
		END
 | 
			
		||||
	},
 | 
			
		||||
	q{
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		-- Удаление записи из files, если удалён последний snapshot
 | 
			
		||||
		-- ------------------------------------------------------------
 | 
			
		||||
		CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_file
 | 
			
		||||
		AFTER DELETE ON snapshots
 | 
			
		||||
		FOR EACH ROW
 | 
			
		||||
		BEGIN
 | 
			
		||||
			DELETE FROM files
 | 
			
		||||
			WHERE id = OLD.file
 | 
			
		||||
			AND NOT EXISTS (
 | 
			
		||||
				SELECT 1 FROM snapshots WHERE file = OLD.file
 | 
			
		||||
			);
 | 
			
		||||
		END;
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -167,9 +167,9 @@ public:
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	/// User-defined label.
 | 
			
		||||
	@property string label() const @safe
 | 
			
		||||
	@property string file() const @safe
 | 
			
		||||
	{
 | 
			
		||||
		return _snapshot.label;
 | 
			
		||||
		return _snapshot.file;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Creation timestamp (UTC) from the database.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,14 @@ import cdcdb.snapshot;
 | 
			
		|||
 | 
			
		||||
import zstd : compress, Level;
 | 
			
		||||
 | 
			
		||||
struct Context {
 | 
			
		||||
	long uid;
 | 
			
		||||
	long ruid;
 | 
			
		||||
	string uidName;
 | 
			
		||||
	string ruidName;
 | 
			
		||||
	string process;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * High-level storage facade: splits data into CDC chunks, stores chunks/blobs
 | 
			
		||||
 * into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot`
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +105,7 @@ public:
 | 
			
		|||
	///
 | 
			
		||||
	/// Throws:
 | 
			
		||||
	///   Exception if `data` is empty or on database/storage errors
 | 
			
		||||
	Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init)
 | 
			
		||||
	Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init)
 | 
			
		||||
	{
 | 
			
		||||
		if (data.length == 0)
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			@ -109,21 +117,9 @@ public:
 | 
			
		|||
		ubyte[32] sha256 = digest!SHA256(data);
 | 
			
		||||
 | 
			
		||||
		// If the last snapshot for the label matches current content
 | 
			
		||||
		if (_db.isLast(label, sha256))
 | 
			
		||||
		if (_db.isLast(file, sha256))
 | 
			
		||||
			return null;
 | 
			
		||||
 | 
			
		||||
		DBSnapshot dbSnapshot;
 | 
			
		||||
 | 
			
		||||
		dbSnapshot.label = label;
 | 
			
		||||
		dbSnapshot.sha256 = sha256;
 | 
			
		||||
		dbSnapshot.description = description;
 | 
			
		||||
		dbSnapshot.sourceLength = data.length;
 | 
			
		||||
		dbSnapshot.algoMin = _minSize;
 | 
			
		||||
		dbSnapshot.algoNormal = _normalSize;
 | 
			
		||||
		dbSnapshot.algoMax = _maxSize;
 | 
			
		||||
		dbSnapshot.maskS = _maskS;
 | 
			
		||||
		dbSnapshot.maskL = _maskL;
 | 
			
		||||
 | 
			
		||||
		_db.beginImmediate();
 | 
			
		||||
 | 
			
		||||
		bool ok;
 | 
			
		||||
| 
						 | 
				
			
			@ -138,6 +134,30 @@ 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);
 | 
			
		||||
 | 
			
		||||
		DBSnapshot dbSnapshot;
 | 
			
		||||
 | 
			
		||||
		dbSnapshot.file = file;
 | 
			
		||||
		dbSnapshot.sha256 = sha256;
 | 
			
		||||
		dbSnapshot.description = description;
 | 
			
		||||
		dbSnapshot.sourceLength = data.length;
 | 
			
		||||
		dbSnapshot.algoMin = _minSize;
 | 
			
		||||
		dbSnapshot.algoNormal = _normalSize;
 | 
			
		||||
		dbSnapshot.algoMax = _maxSize;
 | 
			
		||||
		dbSnapshot.maskS = _maskS;
 | 
			
		||||
		dbSnapshot.maskL = _maskL;
 | 
			
		||||
 | 
			
		||||
		dbSnapshot.uid = context.uid;
 | 
			
		||||
		dbSnapshot.ruid = context.ruid;
 | 
			
		||||
		dbSnapshot.process = context.process;
 | 
			
		||||
 | 
			
		||||
		auto idSnapshot = _db.addSnapshot(dbSnapshot);
 | 
			
		||||
 | 
			
		||||
		DBSnapshotChunk dbSnapshotChunk;
 | 
			
		||||
| 
						 | 
				
			
			@ -193,8 +213,8 @@ public:
 | 
			
		|||
	///   label = snapshot label
 | 
			
		||||
	///
 | 
			
		||||
	/// Returns: number of deleted snapshots
 | 
			
		||||
	long removeSnapshots(string label) {
 | 
			
		||||
		return _db.deleteSnapshot(label);
 | 
			
		||||
	long removeSnapshots(string file) {
 | 
			
		||||
		return _db.deleteSnapshots(file);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Delete a specific snapshot instance.
 | 
			
		||||
| 
						 | 
				
			
			@ -203,8 +223,8 @@ public:
 | 
			
		|||
	///   snapshot = `Snapshot` to remove
 | 
			
		||||
	///
 | 
			
		||||
	/// Returns: `true` on success, `false` otherwise
 | 
			
		||||
	bool removeSnapshots(Snapshot snapshot) {
 | 
			
		||||
		return removeSnapshots(snapshot.id);
 | 
			
		||||
	bool removeSnapshot(Snapshot snapshot) {
 | 
			
		||||
		return removeSnapshot(snapshot.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/// Delete a snapshot by id.
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +233,7 @@ public:
 | 
			
		|||
	///   idSnapshot = snapshot id
 | 
			
		||||
	///
 | 
			
		||||
	/// Returns: `true` if the row was deleted
 | 
			
		||||
	bool removeSnapshots(long idSnapshot) {
 | 
			
		||||
	bool removeSnapshot(long idSnapshot) {
 | 
			
		||||
		return _db.deleteSnapshot(idSnapshot) == idSnapshot;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -233,10 +253,10 @@ public:
 | 
			
		|||
	///   label = filter by exact label; empty string returns all
 | 
			
		||||
	///
 | 
			
		||||
	/// Returns: array of `Snapshot` handles
 | 
			
		||||
	Snapshot[] getSnapshots(string label = string.init) {
 | 
			
		||||
	Snapshot[] getSnapshots(string file = string.init) {
 | 
			
		||||
		Snapshot[] snapshots;
 | 
			
		||||
		
 | 
			
		||||
		foreach (snapshot; _db.getSnapshots(label)) {
 | 
			
		||||
		foreach (snapshot; _db.getSnapshots(file)) {
 | 
			
		||||
			snapshots ~= new Snapshot(_db, snapshot);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								test/app.d
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								test/app.d
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -8,6 +8,18 @@ void main()
 | 
			
		|||
	// Создаем временную базу для примера
 | 
			
		||||
	string dbPath = "./bin/example.db";
 | 
			
		||||
 | 
			
		||||
	if (exists(dbPath)) {
 | 
			
		||||
		remove(dbPath);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Context context;
 | 
			
		||||
 | 
			
		||||
	context.uid = 1000;
 | 
			
		||||
	context.ruid = 1001;
 | 
			
		||||
	context.uidName = "user1";
 | 
			
		||||
	context.ruidName = "user2";
 | 
			
		||||
	context.process = "mcedit";
 | 
			
		||||
 | 
			
		||||
	// Инициализация Storage с компрессией Zstd
 | 
			
		||||
	auto storage = new Storage(dbPath, true, Level.speed);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,17 +31,17 @@ void main()
 | 
			
		|||
	ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
 | 
			
		||||
 | 
			
		||||
	// Создание первого снимка
 | 
			
		||||
	auto snap1 = storage.newSnapshot("example_file", data1, "Версия 1.0");
 | 
			
		||||
	auto snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
 | 
			
		||||
	if (snap1)
 | 
			
		||||
	{
 | 
			
		||||
		writeln("Создан снимок с ID: ", snap1.id);
 | 
			
		||||
		writeln("Метка: ", snap1.label);
 | 
			
		||||
		writeln("Файл: ", snap1.file);
 | 
			
		||||
		writeln("Размер: ", snap1.length, " байт");
 | 
			
		||||
		writeln("Статус: ", snap1.status);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Создание второго снимка (обновление)
 | 
			
		||||
	auto snap2 = storage.newSnapshot("example_file", data2, "Версия 2.0");
 | 
			
		||||
	auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
 | 
			
		||||
	if (snap2)
 | 
			
		||||
	{
 | 
			
		||||
		writeln("Создан обновленный снимок с ID: ", snap2.id);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue