Переписана под ООП
This commit is contained in:
		
							parent
							
								
									46138c032a
								
							
						
					
					
						commit
						8a9142234e
					
				
					 14 changed files with 372 additions and 526 deletions
				
			
		
							
								
								
									
										3
									
								
								dub.json
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								dub.json
									
										
									
									
									
								
							| 
						 | 
					@ -11,8 +11,7 @@
 | 
				
			||||||
		"zstd": "~>0.2.1"
 | 
							"zstd": "~>0.2.1"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"stringImportPaths": [
 | 
						"stringImportPaths": [
 | 
				
			||||||
		"source/cdcdb/db",
 | 
							"source/cdcdb"
 | 
				
			||||||
		"source/cdcdb/cdc"
 | 
					 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
	"configurations": [
 | 
						"configurations": [
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,185 +0,0 @@
 | 
				
			||||||
module cdcdb.cdc.cas;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import cdcdb.db;
 | 
					 | 
				
			||||||
import cdcdb.cdc.core;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import zstd;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import std.digest.sha : SHA256, digest;
 | 
					 | 
				
			||||||
import std.format : format;
 | 
					 | 
				
			||||||
import std.exception : enforce;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Content-Addressable Storage (Контентно-адресуемая система хранения)
 | 
					 | 
				
			||||||
// CAS-хранилище со снапшотами
 | 
					 | 
				
			||||||
final class CAS
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
private:
 | 
					 | 
				
			||||||
	DBLite _db;
 | 
					 | 
				
			||||||
	bool _zstd;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	size_t _minSize;
 | 
					 | 
				
			||||||
	size_t _normalSize;
 | 
					 | 
				
			||||||
	size_t _maxSize;
 | 
					 | 
				
			||||||
	size_t _maskS;
 | 
					 | 
				
			||||||
	size_t _maskL;
 | 
					 | 
				
			||||||
	CDC _cdc;
 | 
					 | 
				
			||||||
public:
 | 
					 | 
				
			||||||
	this(
 | 
					 | 
				
			||||||
		string database,
 | 
					 | 
				
			||||||
		bool zstd = false,
 | 
					 | 
				
			||||||
		size_t busyTimeout = 3000,
 | 
					 | 
				
			||||||
		size_t maxRetries = 3,
 | 
					 | 
				
			||||||
		size_t minSize = 256,
 | 
					 | 
				
			||||||
		size_t normalSize = 512,
 | 
					 | 
				
			||||||
		size_t maxSize = 1024,
 | 
					 | 
				
			||||||
		size_t maskS = 0xFF,
 | 
					 | 
				
			||||||
		size_t maskL = 0x0F
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		_db = new DBLite(database, busyTimeout, maxRetries);
 | 
					 | 
				
			||||||
		_zstd = zstd;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		_minSize = minSize;
 | 
					 | 
				
			||||||
		_normalSize = normalSize;
 | 
					 | 
				
			||||||
		_maxSize = maxSize;
 | 
					 | 
				
			||||||
		_maskS = maskS;
 | 
					 | 
				
			||||||
		_maskL = maskL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	size_t newSnapshot(string label, const(ubyte)[] data, string description = string.init)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		if (data.length == 0) {
 | 
					 | 
				
			||||||
			throw new Exception("Данные имеют нулевой размер");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ubyte[32] sha256 = digest!SHA256(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Если последний снимок файла соответствует текущему состоянию
 | 
					 | 
				
			||||||
		if (_db.isLast(label, sha256)) return 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Snapshot snapshot;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		snapshot.label = label;
 | 
					 | 
				
			||||||
		snapshot.sha256 = sha256;
 | 
					 | 
				
			||||||
		snapshot.description = description;
 | 
					 | 
				
			||||||
		snapshot.sourceLength = data.length;
 | 
					 | 
				
			||||||
		snapshot.algoMin = _minSize;
 | 
					 | 
				
			||||||
		snapshot.algoNormal = _normalSize;
 | 
					 | 
				
			||||||
		snapshot.algoMax = _maxSize;
 | 
					 | 
				
			||||||
		snapshot.maskS = _maskS;
 | 
					 | 
				
			||||||
		snapshot.maskL = _maskL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		_db.beginImmediate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		bool ok;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		scope (exit)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if (!ok)
 | 
					 | 
				
			||||||
				_db.rollback();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		scope (success)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			_db.commit();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		auto idSnapshot = _db.addSnapshot(snapshot);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		SnapshotChunk snapshotChunk;
 | 
					 | 
				
			||||||
		Blob blob;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		blob.zstd = _zstd;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Разбить на фрагменты
 | 
					 | 
				
			||||||
		Chunk[] chunks = _cdc.split(data);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Запись фрагментов в БД
 | 
					 | 
				
			||||||
		foreach (chunk; chunks)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			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.sha256 = chunk.sha256;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Привязка фрагментов к снимку
 | 
					 | 
				
			||||||
			_db.addSnapshotChunk(snapshotChunk);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ok = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return idSnapshot;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Snapshot[] getSnapshots(string label = string.init)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		return _db.getSnapshots(label);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	ubyte[] getSnapshotData(const ref Snapshot snapshot)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		auto dataChunks = _db.getChunks(snapshot.id);
 | 
					 | 
				
			||||||
		ubyte[] content;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach (chunk; dataChunks) {
 | 
					 | 
				
			||||||
			ubyte[] bytes;
 | 
					 | 
				
			||||||
			if (chunk.zstd) {
 | 
					 | 
				
			||||||
				enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
 | 
					 | 
				
			||||||
				bytes = cast(ubyte[]) uncompress(chunk.content);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				bytes = chunk.content;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
 | 
					 | 
				
			||||||
			content ~= bytes;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		enforce(snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return content;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	void removeSnapshot(const ref Snapshot snapshot)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		_db.beginImmediate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		bool ok;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		scope (exit)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			if (!ok)
 | 
					 | 
				
			||||||
				_db.rollback();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		scope (success)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			_db.commit();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		_db.deleteSnapshot(snapshot.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ok = true;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	string getVersion() const @safe nothrow {
 | 
					 | 
				
			||||||
		import cdcdb.version_;
 | 
					 | 
				
			||||||
		return cdcdbVersion;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
module cdcdb.cdc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public import cdcdb.cdc.cas;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
module cdcdb.cdc.core;
 | 
					module cdcdb.core;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import std.digest.sha : SHA256, digest;
 | 
					import std.digest.sha : SHA256, digest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +0,0 @@
 | 
				
			||||||
module cdcdb.db;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public import cdcdb.db.dblite;
 | 
					 | 
				
			||||||
public import cdcdb.db.types;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,197 +0,0 @@
 | 
				
			||||||
# Схемы базы данных для хранения снимков (фрагментов)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Структура базы данных
 | 
					 | 
				
			||||||
```mermaid
 | 
					 | 
				
			||||||
erDiagram
 | 
					 | 
				
			||||||
  %% Композитный PK у SNAPSHOT_CHUNKS: (snapshot_id, chunk_index)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  SNAPSHOTS {
 | 
					 | 
				
			||||||
    int    id PK
 | 
					 | 
				
			||||||
    string label
 | 
					 | 
				
			||||||
    string created_utc
 | 
					 | 
				
			||||||
    int    source_length
 | 
					 | 
				
			||||||
    int    algo_min
 | 
					 | 
				
			||||||
    int    algo_normal
 | 
					 | 
				
			||||||
    int    algo_max
 | 
					 | 
				
			||||||
    int    mask_s
 | 
					 | 
				
			||||||
    int    mask_l
 | 
					 | 
				
			||||||
    string status
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  BLOBS {
 | 
					 | 
				
			||||||
    string sha256 PK
 | 
					 | 
				
			||||||
    int    size
 | 
					 | 
				
			||||||
    blob   content
 | 
					 | 
				
			||||||
    string created_utc
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  SNAPSHOT_CHUNKS {
 | 
					 | 
				
			||||||
    int    snapshot_id FK
 | 
					 | 
				
			||||||
    int    chunk_index
 | 
					 | 
				
			||||||
    int    offset
 | 
					 | 
				
			||||||
    int    size
 | 
					 | 
				
			||||||
    string sha256 FK
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  %% Связи и поведение внешних ключей
 | 
					 | 
				
			||||||
  SNAPSHOTS ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE CASCADE"
 | 
					 | 
				
			||||||
  BLOBS     ||--o{ SNAPSHOT_CHUNKS : "1:N, ON DELETE RESTRICT"
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Схема последовательности записи в базу данных
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```mermaid
 | 
					 | 
				
			||||||
sequenceDiagram
 | 
					 | 
				
			||||||
    autonumber
 | 
					 | 
				
			||||||
    participant APP as Приложение
 | 
					 | 
				
			||||||
    participant CH as Разбиение на чанки (FastCDC)
 | 
					 | 
				
			||||||
    participant HS as Хеширование (SHA-256)
 | 
					 | 
				
			||||||
    participant DB as База данных (SQLite)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Подготовка
 | 
					 | 
				
			||||||
    APP->>DB: Открывает соединение, включает PRAGMA (WAL, foreign_keys=ON)
 | 
					 | 
				
			||||||
    APP->>DB: BEGIN IMMEDIATE (начать транзакцию с блокировкой на запись)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Создание метаданных снимка
 | 
					 | 
				
			||||||
    APP->>DB: INSERT INTO snapshots(label, source_length, algo_min, algo_normal, algo_max, mask_s, mask_l, status='pending')
 | 
					 | 
				
			||||||
    DB-->>APP: id снимка = last_insert_rowid()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,CH: Поток файла → чанки
 | 
					 | 
				
			||||||
    APP->>CH: Читает файл, передает параметры FastCDC (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: digest (sha256)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Note over APP,DB: Дедупликация контента
 | 
					 | 
				
			||||||
        APP->>DB: SELECT 1 FROM blobs WHERE sha256 = ?
 | 
					 | 
				
			||||||
        alt Блоб отсутствует
 | 
					 | 
				
			||||||
            APP->>DB: INSERT INTO blobs(sha256, size, content)
 | 
					 | 
				
			||||||
            DB-->>APP: OK
 | 
					 | 
				
			||||||
        else Блоб уже есть
 | 
					 | 
				
			||||||
            DB-->>APP: Найден (пропускаем вставку содержимого)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Note over APP,DB: Привязка чанка к снимку
 | 
					 | 
				
			||||||
        APP->>DB: INSERT INTO snapshot_chunks(snapshot_id, chunk_index, offset, size, sha256)
 | 
					 | 
				
			||||||
        DB-->>APP: OK (PK: (snapshot_id, chunk_index))
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Валидация и завершение
 | 
					 | 
				
			||||||
    APP->>DB: SELECT SUM(size) FROM snapshot_chunks WHERE snapshot_id = ?
 | 
					 | 
				
			||||||
    DB-->>APP: total_size
 | 
					 | 
				
			||||||
    alt total_size == snapshots.source_length
 | 
					 | 
				
			||||||
        APP->>DB: UPDATE snapshots SET status='ready' WHERE id = ?
 | 
					 | 
				
			||||||
        APP->>DB: COMMIT
 | 
					 | 
				
			||||||
        DB-->>APP: Транзакция зафиксирована
 | 
					 | 
				
			||||||
    else Несоответствие размеров или ошибка
 | 
					 | 
				
			||||||
        APP->>DB: ROLLBACK
 | 
					 | 
				
			||||||
        DB-->>APP: Откат изменений
 | 
					 | 
				
			||||||
        APP-->>APP: Логирует ошибку, возвращает код/исключение
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Схема последовательности восстановления из базы данных
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```mermaid
 | 
					 | 
				
			||||||
sequenceDiagram
 | 
					 | 
				
			||||||
    autonumber
 | 
					 | 
				
			||||||
    participant APP as Приложение
 | 
					 | 
				
			||||||
    participant DB as База данных (SQLite)
 | 
					 | 
				
			||||||
    participant FS as Целевой файл
 | 
					 | 
				
			||||||
    participant HS as Хеширование (опц.)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Подготовка к чтению
 | 
					 | 
				
			||||||
    APP->>DB: Открывает соединение (read), BEGIN (снимок чтения)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Выбор снимка
 | 
					 | 
				
			||||||
    APP->>DB: Находит нужный снимок по id/label, читает status и source_length
 | 
					 | 
				
			||||||
    DB-->>APP: id, status, source_length
 | 
					 | 
				
			||||||
    alt status == "ready"
 | 
					 | 
				
			||||||
    else снимок не готов
 | 
					 | 
				
			||||||
        APP-->>APP: Прерывает восстановление с ошибкой
 | 
					 | 
				
			||||||
        DB-->>APP: END
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Получение состава снимка
 | 
					 | 
				
			||||||
    APP->>DB: SELECT chunk_index, offset, size, sha256 FROM snapshot_chunks WHERE snapshot_id=? ORDER BY chunk_index
 | 
					 | 
				
			||||||
    DB-->>APP: Строки чанков в порядке chunk_index
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    loop Для каждого чанка
 | 
					 | 
				
			||||||
        APP->>DB: SELECT content, size FROM blobs WHERE sha256=?
 | 
					 | 
				
			||||||
        DB-->>APP: content, blob_size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Note over APP,HS: (опц.) контроль целостности чанка
 | 
					 | 
				
			||||||
        APP->>HS: Вычисляет SHA-256(content)
 | 
					 | 
				
			||||||
        HS-->>APP: digest
 | 
					 | 
				
			||||||
        APP-->>APP: Сверяет digest с sha256 и size с blob_size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        alt offset задан
 | 
					 | 
				
			||||||
            APP->>FS: Позиционируется на offset и пишет content (pwrite/seek+write)
 | 
					 | 
				
			||||||
        else offset отсутствует
 | 
					 | 
				
			||||||
            APP->>FS: Дописывает content в конец файла
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Note over APP,DB: Финальная проверка
 | 
					 | 
				
			||||||
    APP-->>APP: Суммирует размеры записанных чанков → total_size
 | 
					 | 
				
			||||||
    APP->>DB: Берёт snapshots.source_length
 | 
					 | 
				
			||||||
    DB-->>APP: source_length
 | 
					 | 
				
			||||||
    alt total_size == source_length
 | 
					 | 
				
			||||||
        APP->>FS: fsync и close
 | 
					 | 
				
			||||||
        DB-->>APP: END
 | 
					 | 
				
			||||||
        APP-->>APP: Успешное восстановление
 | 
					 | 
				
			||||||
    else размеры не совпали
 | 
					 | 
				
			||||||
        APP->>FS: Удаляет/помечает файл как повреждённый
 | 
					 | 
				
			||||||
        DB-->>APP: END
 | 
					 | 
				
			||||||
        APP-->>APP: Фиксирует ошибку (несоответствие сумм)
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,57 +0,0 @@
 | 
				
			||||||
module cdcdb.db.types;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import std.datetime : DateTime;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum SnapshotStatus : int
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	pending = 0,
 | 
					 | 
				
			||||||
	ready = 1
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Snapshot
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	long id;
 | 
					 | 
				
			||||||
	string label;
 | 
					 | 
				
			||||||
	ubyte[32] sha256;
 | 
					 | 
				
			||||||
	string description;
 | 
					 | 
				
			||||||
	DateTime createdUtc;
 | 
					 | 
				
			||||||
	long sourceLength;
 | 
					 | 
				
			||||||
	long algoMin;
 | 
					 | 
				
			||||||
	long algoNormal;
 | 
					 | 
				
			||||||
	long algoMax;
 | 
					 | 
				
			||||||
	long maskS;
 | 
					 | 
				
			||||||
	long maskL;
 | 
					 | 
				
			||||||
	SnapshotStatus status;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Blob
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	ubyte[32] sha256;
 | 
					 | 
				
			||||||
	ubyte[32] zSha256;
 | 
					 | 
				
			||||||
	long size;
 | 
					 | 
				
			||||||
	long zSize;
 | 
					 | 
				
			||||||
	ubyte[] content;
 | 
					 | 
				
			||||||
	DateTime createdUtc;
 | 
					 | 
				
			||||||
	DateTime lastSeenUtc;
 | 
					 | 
				
			||||||
	long refcount;
 | 
					 | 
				
			||||||
	bool zstd;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct SnapshotChunk
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	long snapshotId;
 | 
					 | 
				
			||||||
	long chunkIndex;
 | 
					 | 
				
			||||||
	long offset;
 | 
					 | 
				
			||||||
	ubyte[32] sha256;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct SnapshotDataChunk {
 | 
					 | 
				
			||||||
	long chunkIndex;
 | 
					 | 
				
			||||||
	long offset;
 | 
					 | 
				
			||||||
	long size;
 | 
					 | 
				
			||||||
	ubyte[] content;
 | 
					 | 
				
			||||||
	bool zstd;
 | 
					 | 
				
			||||||
	long zSize;
 | 
					 | 
				
			||||||
	ubyte[32] sha256;
 | 
					 | 
				
			||||||
	ubyte[32] zSha256;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,66 @@
 | 
				
			||||||
module cdcdb.db.dblite;
 | 
					module cdcdb.dblite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import cdcdb.db.types;
 | 
					import arsd.sqlite : Sqlite, SqliteResult, DatabaseException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import arsd.sqlite;
 | 
					import std.datetime : DateTime;
 | 
				
			||||||
 | 
					 | 
				
			||||||
import std.exception : enforce;
 | 
					 | 
				
			||||||
import std.conv : to;
 | 
					 | 
				
			||||||
import std.string : join, replace, toLower;
 | 
					import std.string : join, replace, toLower;
 | 
				
			||||||
import std.algorithm : canFind;
 | 
					import std.algorithm : canFind;
 | 
				
			||||||
 | 
					import std.conv : to;
 | 
				
			||||||
import std.format : format;
 | 
					import std.format : format;
 | 
				
			||||||
 | 
					import std.exception : enforce;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SnapshotStatus : ubyte
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						pending = 0,
 | 
				
			||||||
 | 
						ready = 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct DBSnapshot {
 | 
				
			||||||
 | 
						long id;
 | 
				
			||||||
 | 
						string label;
 | 
				
			||||||
 | 
						ubyte[32] sha256;
 | 
				
			||||||
 | 
						string description;
 | 
				
			||||||
 | 
						DateTime createdUtc;
 | 
				
			||||||
 | 
						long sourceLength;
 | 
				
			||||||
 | 
						long algoMin;
 | 
				
			||||||
 | 
						long algoNormal;
 | 
				
			||||||
 | 
						long algoMax;
 | 
				
			||||||
 | 
						long maskS;
 | 
				
			||||||
 | 
						long maskL;
 | 
				
			||||||
 | 
						SnapshotStatus status;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct DBSnapshotChunk
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						long snapshotId;
 | 
				
			||||||
 | 
						long chunkIndex;
 | 
				
			||||||
 | 
						long offset;
 | 
				
			||||||
 | 
						ubyte[32] sha256;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct DBBlob
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						ubyte[32] sha256;
 | 
				
			||||||
 | 
						ubyte[32] zSha256;
 | 
				
			||||||
 | 
						long size;
 | 
				
			||||||
 | 
						long zSize;
 | 
				
			||||||
 | 
						ubyte[] content;
 | 
				
			||||||
 | 
						DateTime createdUtc;
 | 
				
			||||||
 | 
						DateTime lastSeenUtc;
 | 
				
			||||||
 | 
						long refcount;
 | 
				
			||||||
 | 
						bool zstd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct DBSnapshotChunkData {
 | 
				
			||||||
 | 
						long chunkIndex;
 | 
				
			||||||
 | 
						long offset;
 | 
				
			||||||
 | 
						long size;
 | 
				
			||||||
 | 
						ubyte[] content;
 | 
				
			||||||
 | 
						bool zstd;
 | 
				
			||||||
 | 
						long zSize;
 | 
				
			||||||
 | 
						ubyte[32] sha256;
 | 
				
			||||||
 | 
						ubyte[32] zSha256;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final class DBLite : Sqlite
 | 
					final class DBLite : Sqlite
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -120,7 +172,25 @@ public:
 | 
				
			||||||
		sql("ROLLBACK");
 | 
							sql("ROLLBACK");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	long addSnapshot(Snapshot snapshot)
 | 
						bool isLast(string label, ubyte[] sha256) {
 | 
				
			||||||
 | 
							auto queryResult = sql(
 | 
				
			||||||
 | 
								q{
 | 
				
			||||||
 | 
									SELECT COALESCE(
 | 
				
			||||||
 | 
										(SELECT (label = ? AND sha256 = ?)
 | 
				
			||||||
 | 
										FROM snapshots
 | 
				
			||||||
 | 
										ORDER BY created_utc DESC
 | 
				
			||||||
 | 
										LIMIT 1),
 | 
				
			||||||
 | 
										0
 | 
				
			||||||
 | 
									) AS is_last;
 | 
				
			||||||
 | 
								}, label, sha256
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!queryResult.empty())
 | 
				
			||||||
 | 
								return queryResult.front()["is_last"].to!long > 0;
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						long addSnapshot(DBSnapshot snapshot)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		auto queryResult = sql(
 | 
							auto queryResult = sql(
 | 
				
			||||||
			q{
 | 
								q{
 | 
				
			||||||
| 
						 | 
					@ -157,13 +227,14 @@ public:
 | 
				
			||||||
		return queryResult.front()["id"].to!long;
 | 
							return queryResult.front()["id"].to!long;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void addBlob(Blob blob)
 | 
						bool addBlob(DBBlob blob)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		sql(
 | 
							auto queryResult = sql(
 | 
				
			||||||
			q{
 | 
								q{
 | 
				
			||||||
				INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd)
 | 
									INSERT INTO blobs (sha256, z_sha256, size, z_size, content, zstd)
 | 
				
			||||||
				VALUES (?,?,?,?,?,?)
 | 
									VALUES (?,?,?,?,?,?)
 | 
				
			||||||
				ON CONFLICT (sha256) DO NOTHING
 | 
									ON CONFLICT (sha256) DO NOTHING
 | 
				
			||||||
 | 
									RETURNING sha256
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			blob.sha256[],
 | 
								blob.sha256[],
 | 
				
			||||||
			blob.zstd ? blob.zSha256[] : null,
 | 
								blob.zstd ? blob.zSha256[] : null,
 | 
				
			||||||
| 
						 | 
					@ -172,76 +243,28 @@ public:
 | 
				
			||||||
			blob.content,
 | 
								blob.content,
 | 
				
			||||||
			blob.zstd.to!int
 | 
								blob.zstd.to!int
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return !queryResult.empty();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void addSnapshotChunk(SnapshotChunk snapshotChunk)
 | 
						bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		sql(
 | 
							auto queryResult = sql(
 | 
				
			||||||
			q{
 | 
								q{
 | 
				
			||||||
				INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256)
 | 
									INSERT INTO snapshot_chunks (snapshot_id, chunk_index, offset, sha256)
 | 
				
			||||||
				VALUES(?,?,?,?)
 | 
									VALUES(?,?,?,?)
 | 
				
			||||||
 | 
									RETURNING snapshot_id
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			snapshotChunk.snapshotId,
 | 
								snapshotChunk.snapshotId,
 | 
				
			||||||
			snapshotChunk.chunkIndex,
 | 
								snapshotChunk.chunkIndex,
 | 
				
			||||||
			snapshotChunk.offset,
 | 
								snapshotChunk.offset,
 | 
				
			||||||
			snapshotChunk.sha256[]
 | 
								snapshotChunk.sha256[]
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return !queryResult.empty();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	bool isLast(string label, ubyte[] sha256) {
 | 
						DBSnapshot getSnapshot(long id)
 | 
				
			||||||
		auto queryResult = sql(
 | 
					 | 
				
			||||||
			q{
 | 
					 | 
				
			||||||
				SELECT COALESCE(
 | 
					 | 
				
			||||||
					(SELECT (label = ? AND sha256 = ?)
 | 
					 | 
				
			||||||
					FROM snapshots
 | 
					 | 
				
			||||||
					ORDER BY created_utc DESC
 | 
					 | 
				
			||||||
					LIMIT 1),
 | 
					 | 
				
			||||||
					0
 | 
					 | 
				
			||||||
				) AS is_last;
 | 
					 | 
				
			||||||
			}, label, sha256
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!queryResult.empty())
 | 
					 | 
				
			||||||
			return queryResult.front()["is_last"].to!long > 0;
 | 
					 | 
				
			||||||
		return false;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Snapshot[] getSnapshots(string label)
 | 
					 | 
				
			||||||
	{
 | 
					 | 
				
			||||||
		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
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Snapshot[] snapshots;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		foreach (row; queryResult)
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			Snapshot snapshot;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			snapshot.id = row["id"].to!long;
 | 
					 | 
				
			||||||
			snapshot.label = row["label"].to!string;
 | 
					 | 
				
			||||||
			snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
 | 
					 | 
				
			||||||
			snapshot.description = row["description"].to!string;
 | 
					 | 
				
			||||||
			snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
 | 
					 | 
				
			||||||
			snapshot.sourceLength = row["source_length"].to!long;
 | 
					 | 
				
			||||||
			snapshot.algoMin = row["algo_min"].to!long;
 | 
					 | 
				
			||||||
			snapshot.algoNormal = row["algo_normal"].to!long;
 | 
					 | 
				
			||||||
			snapshot.algoMax = row["algo_max"].to!long;
 | 
					 | 
				
			||||||
			snapshot.maskS = row["mask_s"].to!long;
 | 
					 | 
				
			||||||
			snapshot.maskL = row["mask_l"].to!long;
 | 
					 | 
				
			||||||
			snapshot.status = cast(SnapshotStatus) row["status"].to!int;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			snapshots ~= snapshot;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return snapshots;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Snapshot getSnapshot(long id)
 | 
					 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		auto queryResult = sql(
 | 
							auto queryResult = sql(
 | 
				
			||||||
			q{
 | 
								q{
 | 
				
			||||||
| 
						 | 
					@ -251,7 +274,7 @@ public:
 | 
				
			||||||
			}, id
 | 
								}, id
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Snapshot snapshot;
 | 
							DBSnapshot snapshot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!queryResult.empty())
 | 
							if (!queryResult.empty())
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
| 
						 | 
					@ -274,11 +297,42 @@ public:
 | 
				
			||||||
		return snapshot;
 | 
							return snapshot;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	void deleteSnapshot(long id) {
 | 
						DBSnapshot[] getSnapshots(string label)
 | 
				
			||||||
		sql("DELETE FROM snapshots WHERE id = ?", id);
 | 
						{
 | 
				
			||||||
 | 
							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
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							DBSnapshot[] snapshots;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							foreach (row; queryResult)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								DBSnapshot snapshot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								snapshot.id = row["id"].to!long;
 | 
				
			||||||
 | 
								snapshot.label = row["label"].to!string;
 | 
				
			||||||
 | 
								snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
 | 
				
			||||||
 | 
								snapshot.description = row["description"].to!string;
 | 
				
			||||||
 | 
								snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
 | 
				
			||||||
 | 
								snapshot.sourceLength = row["source_length"].to!long;
 | 
				
			||||||
 | 
								snapshot.algoMin = row["algo_min"].to!long;
 | 
				
			||||||
 | 
								snapshot.algoNormal = row["algo_normal"].to!long;
 | 
				
			||||||
 | 
								snapshot.algoMax = row["algo_max"].to!long;
 | 
				
			||||||
 | 
								snapshot.maskS = row["mask_s"].to!long;
 | 
				
			||||||
 | 
								snapshot.maskL = row["mask_l"].to!long;
 | 
				
			||||||
 | 
								snapshot.status = cast(SnapshotStatus) row["status"].to!int;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								snapshots ~= snapshot;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return snapshots;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	SnapshotDataChunk[] getChunks(long snapshotId)
 | 
						DBSnapshotChunkData[] getChunks(long snapshotId)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		auto queryResult = sql(
 | 
							auto queryResult = sql(
 | 
				
			||||||
			q{
 | 
								q{
 | 
				
			||||||
| 
						 | 
					@ -291,11 +345,11 @@ public:
 | 
				
			||||||
			}, snapshotId
 | 
								}, snapshotId
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		SnapshotDataChunk[] sdchs;
 | 
							DBSnapshotChunkData[] sdchs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		foreach (row; queryResult)
 | 
							foreach (row; queryResult)
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			SnapshotDataChunk sdch;
 | 
								DBSnapshotChunkData sdch;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			sdch.chunkIndex = row["chunk_index"].to!long;
 | 
								sdch.chunkIndex = row["chunk_index"].to!long;
 | 
				
			||||||
			sdch.offset = row["offset"].to!long;
 | 
								sdch.offset = row["offset"].to!long;
 | 
				
			||||||
| 
						 | 
					@ -311,4 +365,14 @@ public:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return sdchs;
 | 
							return sdchs;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						long deleteSnapshot(long id) {
 | 
				
			||||||
 | 
							auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (queryResult.empty()) {
 | 
				
			||||||
 | 
								throw new Exception("Ошибка при удалении снимка из базы данных");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return queryResult.front()["id"].to!long;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
module cdcdb;
 | 
					module cdcdb;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public import cdcdb.cdc;
 | 
					public import cdcdb.storage;
 | 
				
			||||||
 | 
					public import cdcdb.snapshot;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										68
									
								
								source/cdcdb/snapshot.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								source/cdcdb/snapshot.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					module cdcdb.snapshot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cdcdb.dblite;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import std.exception : enforce;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class Snapshot {
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						DBLite _db;
 | 
				
			||||||
 | 
						DBSnapshot _snapshot;
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						this(DBLite dblite, DBSnapshot dbSnapshot) {
 | 
				
			||||||
 | 
							_db = dblite;
 | 
				
			||||||
 | 
							_snapshot = dbSnapshot;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						this(DBLite dblite, long idSnapshot) {
 | 
				
			||||||
 | 
							_db = dblite;
 | 
				
			||||||
 | 
							_snapshot = _db.getSnapshot(idSnapshot);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ubyte[] data() {
 | 
				
			||||||
 | 
							auto dataChunks = _db.getChunks(_snapshot.id);
 | 
				
			||||||
 | 
							ubyte[] content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							import zstd : uncompress;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							foreach (chunk; dataChunks) {
 | 
				
			||||||
 | 
								ubyte[] bytes;
 | 
				
			||||||
 | 
								if (chunk.zstd) {
 | 
				
			||||||
 | 
									enforce(chunk.zSize == chunk.content.length, "Размер сжатого фрагмента не соответствует ожидаемому");
 | 
				
			||||||
 | 
									bytes = cast(ubyte[]) uncompress(chunk.content);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									bytes = chunk.content;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								enforce(chunk.size == bytes.length, "Оригинальный размер не соответствует ожидаемому");
 | 
				
			||||||
 | 
								content ~= bytes;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							import std.digest.sha : SHA256, digest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							enforce(_snapshot.sha256 == digest!SHA256(content), "Хеш-сумма файла не совпадает");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return content;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										154
									
								
								source/cdcdb/storage.d
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								source/cdcdb/storage.d
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,154 @@
 | 
				
			||||||
 | 
					module cdcdb.storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cdcdb.dblite;
 | 
				
			||||||
 | 
					import cdcdb.core;
 | 
				
			||||||
 | 
					import cdcdb.snapshot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class Storage
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					private:
 | 
				
			||||||
 | 
						// Параметры работы с базой данных
 | 
				
			||||||
 | 
						DBLite _db;
 | 
				
			||||||
 | 
						bool _zstd;
 | 
				
			||||||
 | 
						// Настройки CDC механизма
 | 
				
			||||||
 | 
						CDC _cdc;
 | 
				
			||||||
 | 
						size_t _minSize;
 | 
				
			||||||
 | 
						size_t _normalSize;
 | 
				
			||||||
 | 
						size_t _maxSize;
 | 
				
			||||||
 | 
						size_t _maskS;
 | 
				
			||||||
 | 
						size_t _maskL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024,
 | 
				
			||||||
 | 
							size_t maskS = 0xFF, size_t maskL = 0x0F)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							_minSize = minSize;
 | 
				
			||||||
 | 
							_normalSize = normalSize;
 | 
				
			||||||
 | 
							_maxSize = maxSize;
 | 
				
			||||||
 | 
							_maskS = maskS;
 | 
				
			||||||
 | 
							_maskL = maskL;
 | 
				
			||||||
 | 
							// CDC не хранит динамически выделенных данных, переинициализация безопасна
 | 
				
			||||||
 | 
							_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public:
 | 
				
			||||||
 | 
						this(string database, bool zstd = false, size_t busyTimeout = 3000, size_t maxRetries = 3)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							_db = new DBLite(database, busyTimeout, maxRetries);
 | 
				
			||||||
 | 
							_zstd = zstd;
 | 
				
			||||||
 | 
							initCDC();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, size_t maskS, size_t maskL)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							initCDC(minSize, normalSize, maxSize, maskS, maskL);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init)
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							if (data.length == 0)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								throw new Exception("Данные имеют нулевой размер");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							import std.digest.sha : SHA256, digest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ubyte[32] sha256 = digest!SHA256(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Если последний снимок файла соответствует текущему состоянию
 | 
				
			||||||
 | 
							if (_db.isLast(label, 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							scope (exit)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								if (!ok)
 | 
				
			||||||
 | 
									_db.rollback();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							scope (success)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								_db.commit();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							auto idSnapshot = _db.addSnapshot(dbSnapshot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							DBSnapshotChunk dbSnapshotChunk;
 | 
				
			||||||
 | 
							DBBlob dbBlob;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							dbBlob.zstd = _zstd;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Разбить на фрагменты
 | 
				
			||||||
 | 
							Chunk[] chunks = _cdc.split(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							import zstd : compress;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Запись фрагментов в БД
 | 
				
			||||||
 | 
							foreach (chunk; chunks)
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								dbBlob.sha256 = chunk.sha256;
 | 
				
			||||||
 | 
								dbBlob.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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									dbBlob.zSize = zSize;
 | 
				
			||||||
 | 
									dbBlob.zSha256 = zHash;
 | 
				
			||||||
 | 
									dbBlob.content = zBytes;
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									dbBlob.content = content.dup;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Запись фрагментов
 | 
				
			||||||
 | 
								_db.addBlob(dbBlob);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								dbSnapshotChunk.snapshotId = idSnapshot;
 | 
				
			||||||
 | 
								dbSnapshotChunk.chunkIndex = chunk.index;
 | 
				
			||||||
 | 
								dbSnapshotChunk.offset = chunk.offset;
 | 
				
			||||||
 | 
								dbSnapshotChunk.sha256 = chunk.sha256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Привязка фрагментов к снимку
 | 
				
			||||||
 | 
								_db.addSnapshotChunk(dbSnapshotChunk);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ok = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Snapshot snapshot = new Snapshot(_db, idSnapshot);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return snapshot;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Snapshot[] getSnapshots(string label = string.init) {
 | 
				
			||||||
 | 
							Snapshot[] snapshots;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							foreach (snapshot; _db.getSnapshots(label)) {
 | 
				
			||||||
 | 
								snapshots ~= new Snapshot(_db, snapshot);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return snapshots;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						string getVersion() const @safe nothrow
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							import cdcdb.version_ : cdcdbVersion;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return cdcdbVersion;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								test/app.d
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								test/app.d
									
										
									
									
									
								
							| 
						 | 
					@ -6,12 +6,18 @@ import std.file : read;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main()
 | 
					void main()
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	auto cas = new CAS("/tmp/base.db", true);
 | 
						auto storage = new Storage("/tmp/base.db", true);
 | 
				
			||||||
	cas.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
						storage.newSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
 | 
				
			||||||
	// import std.stdio : writeln;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	foreach (snapshot; cas.getSnapshots()) {
 | 
						// if (snapshot !is null) {
 | 
				
			||||||
		writeln(snapshot);
 | 
						// 	writeln(cast(string) snapshot.data);
 | 
				
			||||||
 | 
						// 	snapshot.remove();
 | 
				
			||||||
 | 
						// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						import std.stdio : writeln;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						foreach (snapshot; storage.getSnapshots()) {
 | 
				
			||||||
 | 
							writeln(cast(string) snapshot.data);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// writeln(cas.getVersion);
 | 
						// writeln(cas.getVersion);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue