forked from dlang/cdcdb
Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
872e29cba1 | |||
c9623f87e8 | |||
48a9c39caa |
12 changed files with 512 additions and 565 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,13 +1,18 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.1.0] — 2025-09-14
|
## [0.1.1] - 2025-09-14
|
||||||
### Added
|
### Added
|
||||||
- Библиотека для хранения и управления снимками данных на базе SQLite с контентно-зависимым разбиением (FastCDC).
|
- Table `labels` for snapshot labels.
|
||||||
- Дедупликация по SHA-256 чанков, опциональная компрессия Zstd.
|
### Fixed
|
||||||
- Сквозная проверка целостности: хеш каждого чанка и итогового файла.
|
- Improved data integrity with label normalization.
|
||||||
- Поддержка транзакций (WAL), ограничения целостности и триггеры в БД.
|
|
||||||
- Высокоуровневый API:
|
## [0.1.0] — 2025-09-13
|
||||||
- `Storage`: `newSnapshot`, `getSnapshots`, `getSnapshot`, `removeSnapshots`, `removeSnapshot`, `setupCDC`, `getVersion`.
|
### Added
|
||||||
- `Snapshot`: `data()` (возврат буфера) и потоковый `data(void delegate(const(ubyte)[]))`, `remove()`, свойства (`id`, `file`, `created`, `length`, `sha256`, `status`, `description`, `algoMin`, `algoNormal`, `algoMax`, `maskS`, `maskL`, `uid`, `ruid`, `uidName`, `ruidName`, `process`, `isReady`, `isPending`).
|
- SQLite-backed snapshot library with content-defined chunking (FastCDC).
|
||||||
- `Context`: структура для передачи UID, RUID, имён пользователей и процесса при создании снимка.
|
- Deduplication by SHA-256 of chunks; optional Zstd compression.
|
||||||
- Инструмент для генерации Gear-таблицы для FastCDC (`tools/gen.d`).
|
- End-to-end integrity checks: per-chunk hash and final file hash.
|
||||||
|
- Transactions (WAL), basic integrity constraints, and triggers.
|
||||||
|
- High-level API:
|
||||||
|
- `Storage`: `newSnapshot`, `getSnapshots`, `getSnapshot`, `removeSnapshots`, `setupCDC`, `getVersion`.
|
||||||
|
- `Snapshot`: `data()` (buffered) and streaming `data(void delegate(const(ubyte)[]))`, `remove()`, properties (`id`, `label`, `created`, `length`, `sha256`, `status`, `description`).
|
||||||
|
- Tool to generate a Gear table for FastCDC (`tools/gen.d`).
|
||||||
|
|
18
CHANGELOG.ru.md
Normal file
18
CHANGELOG.ru.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.1] - 2025-09-14
|
||||||
|
### Added
|
||||||
|
- Таблица `labels` для меток снимков.
|
||||||
|
### Fixed
|
||||||
|
- Улучшена целостность данных при нормализации меток.
|
||||||
|
|
||||||
|
## [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`).
|
82
README.md
82
README.md
|
@ -1,42 +1,66 @@
|
||||||
# cdcdb
|
# cdcdb
|
||||||
|
|
||||||
Библиотека для версионирования и хранения данных в SQLite с опорой на content-defined chunking (CDC). Алгоритм **FastCDC** используется для разбиения входного потока на чанки переменного размера, что обеспечивает эффективную дедупликацию и экономию места. Поддерживаются опциональное сжатие **Zstd**, транзакции и проверка целостности на основе **SHA-256**. Дополнительно фиксируется контекст выполнения при создании снимков (UID, RUID, процесс). Основные сценарии применения — резервное копирование и контроль версий файлов.
|
A library for storing and managing snapshots of textual data in an SQLite database. It uses content-defined chunking (CDC) based on the FastCDC algorithm to split data into variable-size chunks for efficient deduplication. Supports optional Zstd compression, transactions, and end-to-end integrity verification via SHA-256. Primary use cases: backups and versioning of text files while minimizing storage footprint.
|
||||||
|
|
||||||
## Алгоритм FastCDC
|
## FastCDC algorithm
|
||||||
**FastCDC** — это алгоритм разделения данных на чанки переменной длины, основанный на скользящем хэшировании содержимого. Он использует таблицу **Gear** для вычисления "отпечатков" данных и выбирает точки разреза с учётом минимального, целевого и максимального размеров чанка. Такой подход позволяет эффективно выявлять изменения и сохранять только уникальные части данных.
|
FastCDC splits data into variable-size chunks using content hashing. A Gear table is used to compute rolling “fingerprints” and choose cut points while respecting minimum, target, and maximum chunk sizes. This efficiently detects changes and stores only unique chunks, reducing storage usage.
|
||||||
|
|
||||||
## Архитектура и основные компоненты
|
## Core classes
|
||||||
|
|
||||||
Библиотека разделена на несколько уровней:
|
### Storage
|
||||||
|
High-level API for the SQLite store and snapshot management.
|
||||||
|
|
||||||
- **Storage** — фасад для работы с БД SQLite. Отвечает за разбиение данных (FastCDC), сохранение/удаление снимков, управление блобами и транзакциями.
|
- **Constructor**: Initializes a connection to SQLite.
|
||||||
- **Snapshot** — объект-ссылка на конкретный снимок. Предоставляет доступ к данным (буфером или потоково), а также к метаданным и операциям удаления.
|
- **Methods**:
|
||||||
- **Context** — структура с параметрами окружения (UID, RUID, имена пользователей, процесс), передаваемыми при создании снимка.
|
- `newSnapshot`: Creates a snapshot. Returns a `Snapshot` object or `null` if the data matches the latest snapshot.
|
||||||
|
- `getSnapshots`: Returns a list of snapshots (all, or filtered by label). Returns an array of `Snapshot`.
|
||||||
|
- `getSnapshot`: Fetches a snapshot by ID. Returns a `Snapshot`.
|
||||||
|
- `setupCDC`: Configures CDC splitting parameters. Returns nothing.
|
||||||
|
- `removeSnapshots`: Deletes snapshots by label, ID, or a `Snapshot` object. Returns the number of deleted snapshots (for label) or `true`/`false` (for ID or object).
|
||||||
|
- `getVersion`: Returns the library version string.
|
||||||
|
|
||||||
## Пример использования
|
### Snapshot
|
||||||
|
Work with an individual snapshot.
|
||||||
|
|
||||||
|
- **Constructor**: Creates a snapshot handle by its ID.
|
||||||
|
- **Methods**:
|
||||||
|
- `data`: Restores full snapshot data. Returns a byte array (`ubyte[]`).
|
||||||
|
- `data`: Streams restored data via a delegate sink. Returns nothing.
|
||||||
|
- `remove`: Deletes the snapshot from the database. Returns `true` on success, otherwise `false`.
|
||||||
|
|
||||||
|
- **Properties**:
|
||||||
|
- `id`: Snapshot ID (`long`).
|
||||||
|
- `label`: Snapshot label (`string`).
|
||||||
|
- `created`: Creation timestamp (UTC, `DateTime`).
|
||||||
|
- `length`: Original data length (`long`).
|
||||||
|
- `sha256`: Data SHA-256 hash (`ubyte[32]`).
|
||||||
|
- `status`: Snapshot status (`"pending"` or `"ready"`).
|
||||||
|
- `description`: Snapshot description (`string`).
|
||||||
|
|
||||||
|
## Example
|
||||||
```d
|
```d
|
||||||
import cdcdb;
|
import cdcdb;
|
||||||
|
|
||||||
import std.stdio : writeln, File;
|
import std.stdio : writeln, File;
|
||||||
|
import std.file : exists, remove;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
|
// Create DB
|
||||||
string dbPath = "example.db";
|
string dbPath = "example.db";
|
||||||
|
|
||||||
// Storage с Zstd-сжатием
|
// Initialize Storage with Zstd compression
|
||||||
auto storage = new Storage(dbPath, true, 22);
|
auto storage = new Storage(dbPath, true, 22);
|
||||||
|
|
||||||
// Контекст
|
// Create a snapshot
|
||||||
Context ctx = Context(1000, 1000, "user", "user", "example");
|
|
||||||
|
|
||||||
// Новый снимок
|
|
||||||
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
|
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
|
||||||
auto snap = storage.newSnapshot("example_file", data, ctx, "Версия 1.0");
|
auto snap = storage.newSnapshot("example_file", data, "Version 1.0");
|
||||||
if (snap)
|
if (snap)
|
||||||
{
|
{
|
||||||
writeln("Создан снимок: ID=", snap.id, ", Файл=", snap.file);
|
writeln("Snapshot created: ID=", snap.id, ", Label=", snap.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Восстановление через потоковую запись
|
// Restore data
|
||||||
auto snapshots = storage.getSnapshots("example_file");
|
auto snapshots = storage.getSnapshots("example_file");
|
||||||
if (snapshots.length > 0)
|
if (snapshots.length > 0)
|
||||||
{
|
{
|
||||||
|
@ -44,25 +68,35 @@ void main()
|
||||||
File outFile = File("restored.txt", "wb");
|
File outFile = File("restored.txt", "wb");
|
||||||
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
|
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
|
||||||
outFile.close();
|
outFile.close();
|
||||||
writeln("Данные восстановлены в restored.txt");
|
writeln("Data restored to restored.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаление
|
// Delete snapshots
|
||||||
long deleted = storage.removeSnapshots("example_file");
|
long deleted = storage.removeSnapshots("example_file");
|
||||||
writeln("Удалено снимков: ", deleted);
|
writeln("Deleted snapshots: ", deleted);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Инструменты
|
## Tools
|
||||||
|
|
||||||
В директории `tools` есть скрипт на D для генерации таблицы Gear, используемой FastCDC.
|
The `tools` directory contains a small D script for generating a Gear table used by FastCDC. It lets you build custom hash tables to tune splitting behavior. To generate a new table:
|
||||||
Чтобы создать новую таблицу:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x ./tools/gen.d
|
chmod +x ./tools/gen.d
|
||||||
./tools/gen.d > ./source/gear.d
|
./tools/gen.d > ./source/gear.d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Лицензия
|
## Installation
|
||||||
|
|
||||||
|
* **In `dub.json`**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dependencies": {
|
||||||
|
"cdcdb": "~>0.1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Build**: `dub build`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
Boost Software License 1.0 (BSL-1.0).
|
Boost Software License 1.0 (BSL-1.0).
|
||||||
|
|
98
README.ru.md
Normal file
98
README.ru.md
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
# 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Сборка**: `dub build`.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
Boost Software License 1.0 (BSL-1.0).
|
|
@ -1,45 +1,24 @@
|
||||||
/// Модуль базовых структур и алгоритмов CDC (content-defined chunking).
|
|
||||||
module cdcdb.core;
|
module cdcdb.core;
|
||||||
|
|
||||||
import std.digest.sha : SHA256, digest;
|
import std.digest.sha : SHA256, digest;
|
||||||
|
|
||||||
/// Описание чанка данных.
|
|
||||||
///
|
|
||||||
/// Поля:
|
|
||||||
/// - `index` — порядковый номер чанка, начиная с 1.
|
|
||||||
/// - `offset` — смещение чанка в исходном буфере (в байтах).
|
|
||||||
/// - `size` — размер чанка (в байтах).
|
|
||||||
/// - `sha256` — SHA-256 содержимого (сырые 32 байта).
|
|
||||||
struct Chunk
|
struct Chunk
|
||||||
{
|
{
|
||||||
size_t index;
|
size_t index; // 1..N
|
||||||
size_t offset;
|
size_t offset; // offset in the source buffer
|
||||||
size_t size;
|
size_t size; // chunk size
|
||||||
immutable(ubyte)[32] sha256;
|
immutable(ubyte)[32] sha256; // hex(SHA-256) of the content
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change Data Capture (CDC) — алгоритм нарезки потока на чанки по содержимому.
|
// Change Data Capture (CDC)
|
||||||
///
|
|
||||||
/// Класс реализует скользящее шифрование (rolling hash) с двумя масками:
|
|
||||||
/// строгой (`_maskS`) до «нормального» размера и более слабой (`_maskL`) до «максимального».
|
|
||||||
final class CDC
|
final class CDC
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
size_t _minSize, _normalSize, _maxSize;
|
size_t _minSize, _normalSize, _maxSize;
|
||||||
ulong _maskS, _maskL;
|
ulong _maskS, _maskL;
|
||||||
// Таблица случайных значений Gear (должна быть сгенерирована отдельно в "gear.d")
|
// _gear
|
||||||
mixin(import("gear.d"));
|
mixin(import("gear.d"));
|
||||||
|
|
||||||
/// Вычисляет длину следующего чанка, начиная с начала `src`.
|
|
||||||
///
|
|
||||||
/// Параметры:
|
|
||||||
/// - `src` — оставшийся участок данных.
|
|
||||||
///
|
|
||||||
/// Возвращает: длину чанка в байтах.
|
|
||||||
///
|
|
||||||
/// Детали:
|
|
||||||
/// - Если данных меньше либо равно минимальному размеру — возвращает их длину.
|
|
||||||
/// - Сначала ищется граница по строгой маске до `_normalSize`, затем по слабой до `_maxSize`.
|
|
||||||
size_t cut(const(ubyte)[] src) pure nothrow @safe @nogc
|
size_t cut(const(ubyte)[] src) pure nothrow @safe @nogc
|
||||||
{
|
{
|
||||||
size_t size = src.length;
|
size_t size = src.length;
|
||||||
|
@ -57,13 +36,13 @@ private:
|
||||||
ulong fingerprint = 0;
|
ulong fingerprint = 0;
|
||||||
size_t index;
|
size_t index;
|
||||||
|
|
||||||
// Инициализация без проверки на разрез
|
// initialization without a cut-check
|
||||||
while (index < _minSize)
|
while (index < _minSize)
|
||||||
{
|
{
|
||||||
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
// Строгая маска
|
// strict mask
|
||||||
while (index < normalSize)
|
while (index < normalSize)
|
||||||
{
|
{
|
||||||
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
||||||
|
@ -71,7 +50,7 @@ private:
|
||||||
return index;
|
return index;
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
// Слабая маска
|
// weak mask
|
||||||
while (index < size)
|
while (index < size)
|
||||||
{
|
{
|
||||||
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
fingerprint = (fingerprint << 1) + _gear[src[index]];
|
||||||
|
@ -83,21 +62,10 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Создаёт экземпляр CDC.
|
|
||||||
///
|
|
||||||
/// Параметры:
|
|
||||||
/// - `minSize` — минимальный размер чанка.
|
|
||||||
/// - `normalSize` — целевой (нормальный) размер чанка.
|
|
||||||
/// - `maxSize` — максимальный размер чанка.
|
|
||||||
/// - `maskS` — строгая маска (для поиска границы до `normalSize`).
|
|
||||||
/// - `maskL` — слабая маска (для поиска границы до `maxSize`).
|
|
||||||
///
|
|
||||||
/// Замечания:
|
|
||||||
/// - Требуется `0 < minSize < normalSize < maxSize`.
|
|
||||||
this(size_t minSize, size_t normalSize, size_t maxSize, ulong maskS, ulong maskL) @safe @nogc nothrow
|
this(size_t minSize, size_t normalSize, size_t maxSize, ulong maskS, ulong maskL) @safe @nogc nothrow
|
||||||
{
|
{
|
||||||
assert(minSize > 0 && minSize < normalSize && normalSize < maxSize,
|
assert(minSize > 0 && minSize < normalSize && normalSize < maxSize,
|
||||||
"Некорректные размеры: требуется 0 < min < normal < max");
|
"Invalid sizes: require min < normal < max and min > 0");
|
||||||
_minSize = minSize;
|
_minSize = minSize;
|
||||||
_normalSize = normalSize;
|
_normalSize = normalSize;
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
|
@ -105,12 +73,6 @@ public:
|
||||||
_maskL = maskL;
|
_maskL = maskL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Разбивает буфер `data` на последовательность чанков.
|
|
||||||
///
|
|
||||||
/// Параметры:
|
|
||||||
/// - `data` — исходные байты.
|
|
||||||
///
|
|
||||||
/// Возвращает: массив `Chunk` в порядке следования.
|
|
||||||
Chunk[] split(const(ubyte)[] data) @safe nothrow
|
Chunk[] split(const(ubyte)[] data) @safe nothrow
|
||||||
{
|
{
|
||||||
Chunk[] chunks;
|
Chunk[] chunks;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/// Лёгкая обёртка над SQLite с повторными попытками, схемой БД
|
|
||||||
/// и удобными структурами данных для снимков/чанков/блобов.
|
|
||||||
module cdcdb.dblite;
|
module cdcdb.dblite;
|
||||||
|
|
||||||
import arsd.sqlite : Sqlite, SqliteResult, DatabaseException;
|
import arsd.sqlite : Sqlite, SqliteResult, DatabaseException;
|
||||||
|
@ -11,85 +9,67 @@ import std.conv : to;
|
||||||
import std.format : format;
|
import std.format : format;
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
|
|
||||||
/// Статус снимка.
|
|
||||||
/// - `pending` — создаётся/заполняется;
|
|
||||||
/// - `ready` — полностью подготовлен.
|
|
||||||
enum SnapshotStatus : ubyte
|
enum SnapshotStatus : ubyte
|
||||||
{
|
{
|
||||||
pending = 0,
|
pending = 0,
|
||||||
ready = 1
|
ready = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Запись о снимке в БД (агрегированные метаданные).
|
|
||||||
struct DBSnapshot {
|
struct DBSnapshot {
|
||||||
long id; /// Идентификатор снимка.
|
long id;
|
||||||
string file; /// Имя файла (таблица `files`).
|
string label;
|
||||||
ubyte[32] sha256; /// Хеш всего файла (SHA-256, 32 байта).
|
ubyte[32] sha256;
|
||||||
string description; /// Описание/комментарий (может быть пустым).
|
string description;
|
||||||
DateTime createdUtc; /// Время создания (UTC).
|
DateTime createdUtc;
|
||||||
long sourceLength; /// Длина исходного файла (байт).
|
long sourceLength;
|
||||||
long algoMin; /// FastCDC: минимальный размер чанка.
|
long algoMin;
|
||||||
long algoNormal; /// FastCDC: нормальный (целевой) размер чанка.
|
long algoNormal;
|
||||||
long algoMax; /// FastCDC: максимальный размер чанка.
|
long algoMax;
|
||||||
long maskS; /// Строгая маска FastCDC.
|
long maskS;
|
||||||
long maskL; /// Слабая маска FastCDC.
|
long maskL;
|
||||||
SnapshotStatus status; /// Статус снимка.
|
SnapshotStatus status;
|
||||||
long uid; /// UID процесса (effective).
|
|
||||||
long ruid; /// Real UID процесса.
|
|
||||||
string uidName; /// Имя пользователя для `uid`.
|
|
||||||
string ruidName; /// Имя пользователя для `ruid`.
|
|
||||||
string process; /// Имя процесса (таблица `processes`).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Связь снимка с чанками (индексы и хеши).
|
|
||||||
struct DBSnapshotChunk
|
struct DBSnapshotChunk
|
||||||
{
|
{
|
||||||
long snapshotId; /// ID снимка.
|
long snapshotId;
|
||||||
long chunkIndex; /// Порядковый номер чанка в снимке.
|
long chunkIndex;
|
||||||
long offset; /// Смещение чанка в файле.
|
long offset;
|
||||||
ubyte[32] sha256; /// Хеш чанка (SHA-256, 32 байта).
|
ubyte[32] sha256;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Запись о блобе (уникальный чанк) в БД.
|
|
||||||
struct DBBlob
|
struct DBBlob
|
||||||
{
|
{
|
||||||
ubyte[32] sha256; /// Хеш исходного содержимого.
|
ubyte[32] sha256;
|
||||||
ubyte[32] zSha256; /// Хеш сжатого содержимого (если zstd=true).
|
ubyte[32] zSha256;
|
||||||
long size; /// Размер исходного содержимого.
|
long size;
|
||||||
long zSize; /// Размер сжатого содержимого.
|
long zSize;
|
||||||
ubyte[] content; /// Контент (если хранится в БД).
|
ubyte[] content;
|
||||||
DateTime createdUtc; /// Время создания (UTC).
|
DateTime createdUtc;
|
||||||
DateTime lastSeenUtc; /// Последний доступ (UTC).
|
DateTime lastSeenUtc;
|
||||||
long refcount; /// Ссылки на блоб (сколькими снимками используется).
|
long refcount;
|
||||||
bool zstd; /// Признак, что `content` хранится в сжатом виде.
|
bool zstd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Расширенная выборка чанков для восстановления.
|
|
||||||
/// Содержит и метаданные, и (возможное) содержимое.
|
|
||||||
struct DBSnapshotChunkData {
|
struct DBSnapshotChunkData {
|
||||||
long chunkIndex; /// Порядковый номер чанка.
|
long chunkIndex;
|
||||||
long offset; /// Смещение в файле.
|
long offset;
|
||||||
long size; /// Размер исходного чанка.
|
long size;
|
||||||
ubyte[] content; /// Содержимое (может быть пустым, если хранится вне БД).
|
ubyte[] content;
|
||||||
bool zstd; /// Сжат ли контент Zstd.
|
bool zstd;
|
||||||
long zSize; /// Размер сжатого контента.
|
long zSize;
|
||||||
ubyte[32] sha256; /// Хеш исходного содержимого.
|
ubyte[32] sha256;
|
||||||
ubyte[32] zSha256; /// Хеш сжатого содержимого.
|
ubyte[32] zSha256;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Простейший клиент SQLite с:
|
|
||||||
/// - автоматической инициализацией схемы (при пустой БД);
|
|
||||||
/// - повторными попытками при блокировках;
|
|
||||||
/// - удобными методами для CRUD по объектам домена.
|
|
||||||
final class DBLite : Sqlite
|
final class DBLite : Sqlite
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
string _dbPath; /// Путь к файлу БД.
|
string _dbPath;
|
||||||
size_t _maxRetries; /// Максимум повторов при `busy/locked`.
|
size_t _maxRetries;
|
||||||
// SQL-схема (массив строковых запросов).
|
// _scheme
|
||||||
mixin(import("scheme.d"));
|
mixin(import("scheme.d"));
|
||||||
|
|
||||||
/// Выполняет SQL с повторными попытками при `locked/busy`.
|
|
||||||
SqliteResult sql(T...)(string queryText, T args)
|
SqliteResult sql(T...)(string queryText, T args)
|
||||||
{
|
{
|
||||||
if (_maxRetries == 0) {
|
if (_maxRetries == 0) {
|
||||||
|
@ -107,7 +87,7 @@ private:
|
||||||
if (msg.toLower.canFind("locked", "busy")) {
|
if (msg.toLower.canFind("locked", "busy")) {
|
||||||
if (--tryNo == 0) {
|
if (--tryNo == 0) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
"Не удалось выполнить запрос к базе данных после %d неудачных попыток: %s"
|
"Failed to connect to the database after %d failed attempts: %s"
|
||||||
.format(_maxRetries, msg)
|
.format(_maxRetries, msg)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -119,14 +99,12 @@ private:
|
||||||
throw new Exception(msg);
|
throw new Exception(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверяет наличие обязательных таблиц.
|
// Check that the database contains the required tables; otherwise create them
|
||||||
/// Если все отсутствуют — создаёт схему; если отсутствует часть — бросает ошибку.
|
|
||||||
void check()
|
void check()
|
||||||
{
|
{
|
||||||
SqliteResult queryResult = sql(
|
SqliteResult queryResult = sql(
|
||||||
q{
|
q{
|
||||||
WITH required(name)
|
WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("labels"))
|
||||||
AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files"))
|
|
||||||
SELECT name AS missing_table
|
SELECT name AS missing_table
|
||||||
FROM required
|
FROM required
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
|
@ -144,11 +122,11 @@ private:
|
||||||
missingTables ~= row["missing_table"].to!string;
|
missingTables ~= row["missing_table"].to!string;
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(missingTables.length == 0 || missingTables.length == 6,
|
enforce(missingTables.length == 0 || missingTables.length == 4,
|
||||||
"База данных повреждена. Отсутствуют таблицы: " ~ missingTables.join(", ")
|
"Database is corrupted. Missing tables: " ~ missingTables.join(", ")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (missingTables.length == 6)
|
if (missingTables.length == 4)
|
||||||
{
|
{
|
||||||
foreach (schemeQuery; _scheme)
|
foreach (schemeQuery; _scheme)
|
||||||
{
|
{
|
||||||
|
@ -157,8 +135,6 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Переводит текстовую дату из SQLite (`YYYY-MM-DD HH:MM:SS.SSS`)
|
|
||||||
/// в `DateTime` (ISO 8601 с `T`).
|
|
||||||
DateTime toDateTime(string sqliteDate)
|
DateTime toDateTime(string sqliteDate)
|
||||||
{
|
{
|
||||||
string isoDate = sqliteDate.replace(" ", "T");
|
string isoDate = sqliteDate.replace(" ", "T");
|
||||||
|
@ -166,12 +142,6 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Открывает БД, проверяет/инициализирует схему и настраивает PRAGMA.
|
|
||||||
///
|
|
||||||
/// Параметры:
|
|
||||||
/// - `database` — путь к файлу БД;
|
|
||||||
/// - `busyTimeout` — таймаут ожидания блокировок (мс);
|
|
||||||
/// - `maxRetries` — число повторов при `busy/locked`.
|
|
||||||
this(string database, size_t busyTimeout, size_t maxRetries)
|
this(string database, size_t busyTimeout, size_t maxRetries)
|
||||||
{
|
{
|
||||||
_dbPath = database;
|
_dbPath = database;
|
||||||
|
@ -187,42 +157,36 @@ public:
|
||||||
query("PRAGMA busy_timeout=%d".format(busyTimeout));
|
query("PRAGMA busy_timeout=%d".format(busyTimeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BEGIN IMMEDIATE.
|
|
||||||
void beginImmediate()
|
void beginImmediate()
|
||||||
{
|
{
|
||||||
sql("BEGIN IMMEDIATE");
|
sql("BEGIN IMMEDIATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// COMMIT.
|
|
||||||
void commit()
|
void commit()
|
||||||
{
|
{
|
||||||
sql("COMMIT");
|
sql("COMMIT");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ROLLBACK.
|
|
||||||
void rollback()
|
void rollback()
|
||||||
{
|
{
|
||||||
sql("ROLLBACK");
|
sql("ROLLBACK");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверяет, совпадает ли последний снимок для `file` с заданным `sha256`.
|
bool isLast(string label, ubyte[] sha256) {
|
||||||
///
|
|
||||||
/// Возвращает `true`, если самый свежий снимок этого файла имеет тот же SHA-256.
|
|
||||||
bool isLast(string file, ubyte[] sha256) {
|
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT COALESCE(
|
SELECT COALESCE(
|
||||||
(
|
(
|
||||||
SELECT (s.sha256 = ?2)
|
SELECT (s.sha256 = ?2)
|
||||||
FROM snapshots s
|
FROM snapshots s
|
||||||
JOIN files f ON f.id = s.file
|
JOIN labels l ON l.id = s.label
|
||||||
WHERE f.name = ?1
|
WHERE l.name = ?1
|
||||||
ORDER BY s.created_utc DESC
|
ORDER BY s.created_utc DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
),
|
),
|
||||||
0
|
0
|
||||||
) AS is_last;
|
) AS is_last;
|
||||||
}, file, sha256
|
}, label, sha256
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!queryResult.empty())
|
if (!queryResult.empty())
|
||||||
|
@ -230,19 +194,15 @@ public:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет новый снимок. Возвращает его `id`.
|
|
||||||
long addSnapshot(DBSnapshot snapshot)
|
long addSnapshot(DBSnapshot snapshot)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
INSERT INTO snapshots(
|
INSERT INTO snapshots(
|
||||||
file,
|
label,
|
||||||
sha256,
|
sha256,
|
||||||
description,
|
description,
|
||||||
source_length,
|
source_length,
|
||||||
uid,
|
|
||||||
ruid,
|
|
||||||
process,
|
|
||||||
algo_min,
|
algo_min,
|
||||||
algo_normal,
|
algo_normal,
|
||||||
algo_max,
|
algo_max,
|
||||||
|
@ -251,19 +211,14 @@ public:
|
||||||
status
|
status
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT id FROM files WHERE name = ?),
|
(SELECT id FROM labels WHERE name = ?),
|
||||||
?,?,?,?,?,
|
?,?,?,?,?,?,?,?,?
|
||||||
(SELECT id FROM processes WHERE name = ?),
|
|
||||||
?,?,?,?,?,?
|
|
||||||
RETURNING id
|
RETURNING id
|
||||||
},
|
},
|
||||||
snapshot.file,
|
snapshot.label,
|
||||||
snapshot.sha256[],
|
snapshot.sha256[],
|
||||||
snapshot.description.length ? snapshot.description : null,
|
snapshot.description.length ? snapshot.description : null,
|
||||||
snapshot.sourceLength,
|
snapshot.sourceLength,
|
||||||
snapshot.uid,
|
|
||||||
snapshot.ruid,
|
|
||||||
snapshot.process,
|
|
||||||
snapshot.algoMin,
|
snapshot.algoMin,
|
||||||
snapshot.algoNormal,
|
snapshot.algoNormal,
|
||||||
snapshot.algoMax,
|
snapshot.algoMax,
|
||||||
|
@ -273,13 +228,12 @@ public:
|
||||||
);
|
);
|
||||||
|
|
||||||
if (queryResult.empty()) {
|
if (queryResult.empty()) {
|
||||||
throw new Exception("Ошибка добавления нового снимка в базу данных");
|
throw new Exception("Error adding a new snapshot to the database");
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryResult.front()["id"].to!long;
|
return queryResult.front()["id"].to!long;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет блоб. Возвращает `true`, если вставка произошла (не было конфликта).
|
|
||||||
bool addBlob(DBBlob blob)
|
bool addBlob(DBBlob blob)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
|
@ -300,12 +254,11 @@ public:
|
||||||
return !queryResult.empty();
|
return !queryResult.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет процесс по имени (идемпотентно).
|
bool addLabel(string name)
|
||||||
bool addProcess(string name)
|
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
INSERT INTO processes (name) VALUES (?)
|
INSERT INTO labels (name) VALUES (?)
|
||||||
ON CONFLICT(name) DO NOTHING
|
ON CONFLICT(name) DO NOTHING
|
||||||
}, name
|
}, name
|
||||||
);
|
);
|
||||||
|
@ -313,34 +266,6 @@ public:
|
||||||
return !queryResult.empty();
|
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавляет пользователя (uid, name) (идемпотентно).
|
|
||||||
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)
|
bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
|
@ -358,23 +283,17 @@ public:
|
||||||
return !queryResult.empty();
|
return !queryResult.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает один снимок по `id`. Если не найден — вернёт пустую структуру.
|
|
||||||
DBSnapshot getSnapshot(long id)
|
DBSnapshot getSnapshot(long id)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
f.name file,
|
l.name label,
|
||||||
s.sha256,
|
s.sha256,
|
||||||
s.description,
|
s.description,
|
||||||
s.created_utc,
|
s.created_utc,
|
||||||
s.source_length,
|
s.source_length,
|
||||||
s.uid,
|
|
||||||
s.ruid,
|
|
||||||
u.name uid_name,
|
|
||||||
r.name ruid_name,
|
|
||||||
p.name process,
|
|
||||||
s.algo_min,
|
s.algo_min,
|
||||||
s.algo_normal,
|
s.algo_normal,
|
||||||
s.algo_max,
|
s.algo_max,
|
||||||
|
@ -382,11 +301,8 @@ public:
|
||||||
s.mask_l,
|
s.mask_l,
|
||||||
s.status
|
s.status
|
||||||
FROM snapshots s
|
FROM snapshots s
|
||||||
JOIN processes p ON p.id = s.process
|
JOIN labels l ON l.id = s.label
|
||||||
JOIN users u ON u.uid = s.uid
|
WHERE s.id = ?
|
||||||
JOIN users r ON r.uid = s.ruid
|
|
||||||
JOIN files f ON f.id = s.file
|
|
||||||
WHERE s.id = ?;
|
|
||||||
}, id
|
}, id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -397,7 +313,7 @@ public:
|
||||||
auto data = queryResult.front();
|
auto data = queryResult.front();
|
||||||
|
|
||||||
snapshot.id = data["id"].to!long;
|
snapshot.id = data["id"].to!long;
|
||||||
snapshot.file = data["file"].to!string;
|
snapshot.label = data["label"].to!string;
|
||||||
snapshot.sha256 = cast(ubyte[]) data["sha256"].dup;
|
snapshot.sha256 = cast(ubyte[]) data["sha256"].dup;
|
||||||
snapshot.description = data["description"].to!string;
|
snapshot.description = data["description"].to!string;
|
||||||
snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
|
snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
|
||||||
|
@ -408,33 +324,22 @@ public:
|
||||||
snapshot.maskS = data["mask_s"].to!long;
|
snapshot.maskS = data["mask_s"].to!long;
|
||||||
snapshot.maskL = data["mask_l"].to!long;
|
snapshot.maskL = data["mask_l"].to!long;
|
||||||
snapshot.status = cast(SnapshotStatus) data["status"].to!int;
|
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;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает список снимков (опционально фильтр по имени файла).
|
DBSnapshot[] getSnapshots(string label)
|
||||||
DBSnapshot[] getSnapshots(string file)
|
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
f.name file,
|
l.name label,
|
||||||
s.sha256,
|
s.sha256,
|
||||||
s.description,
|
s.description,
|
||||||
s.created_utc,
|
s.created_utc,
|
||||||
s.source_length,
|
s.source_length,
|
||||||
s.uid,
|
|
||||||
s.ruid,
|
|
||||||
u.name uid_name,
|
|
||||||
r.name ruid_name,
|
|
||||||
p.name process,
|
|
||||||
s.algo_min,
|
s.algo_min,
|
||||||
s.algo_normal,
|
s.algo_normal,
|
||||||
s.algo_max,
|
s.algo_max,
|
||||||
|
@ -442,12 +347,9 @@ public:
|
||||||
s.mask_l,
|
s.mask_l,
|
||||||
s.status
|
s.status
|
||||||
FROM snapshots s
|
FROM snapshots s
|
||||||
JOIN processes p ON p.id = s.process
|
JOIN labels l ON l.id = s.label AND (length(?) = 0 OR l.name = ?1)
|
||||||
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;
|
ORDER BY s.created_utc, s.id;
|
||||||
}, file
|
}, label
|
||||||
);
|
);
|
||||||
|
|
||||||
DBSnapshot[] snapshots;
|
DBSnapshot[] snapshots;
|
||||||
|
@ -457,7 +359,7 @@ public:
|
||||||
DBSnapshot snapshot;
|
DBSnapshot snapshot;
|
||||||
|
|
||||||
snapshot.id = row["id"].to!long;
|
snapshot.id = row["id"].to!long;
|
||||||
snapshot.file = row["file"].to!string;
|
snapshot.label = row["label"].to!string;
|
||||||
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
|
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
|
||||||
snapshot.description = row["description"].to!string;
|
snapshot.description = row["description"].to!string;
|
||||||
snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
|
snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
|
||||||
|
@ -468,11 +370,6 @@ public:
|
||||||
snapshot.maskS = row["mask_s"].to!long;
|
snapshot.maskS = row["mask_s"].to!long;
|
||||||
snapshot.maskL = row["mask_l"].to!long;
|
snapshot.maskL = row["mask_l"].to!long;
|
||||||
snapshot.status = cast(SnapshotStatus) row["status"].to!int;
|
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;
|
snapshots ~= snapshot;
|
||||||
}
|
}
|
||||||
|
@ -480,7 +377,6 @@ public:
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает последовательность чанков снимка c данными.
|
|
||||||
DBSnapshotChunkData[] getChunks(long snapshotId)
|
DBSnapshotChunkData[] getChunks(long snapshotId)
|
||||||
{
|
{
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
|
@ -515,7 +411,6 @@ public:
|
||||||
return sdchs;
|
return sdchs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет один снимок по `id`. Возвращает `id` удалённой строки или 0.
|
|
||||||
long deleteSnapshot(long id) {
|
long deleteSnapshot(long id) {
|
||||||
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
|
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
|
||||||
|
|
||||||
|
@ -525,14 +420,13 @@ public:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет все снимки по имени файла. Возвращает число удалённых строк.
|
long deleteSnapshot(string label) {
|
||||||
long deleteSnapshots(string file) {
|
|
||||||
auto queryResult = sql(
|
auto queryResult = sql(
|
||||||
q{
|
q{
|
||||||
DELETE FROM snapshots
|
DELETE FROM snapshots
|
||||||
WHERE file = (SELECT id FROM files WHERE name = ?)
|
WHERE label = (SELECT id FROM labels WHERE name = ?)
|
||||||
RETURNING 1;
|
RETURNING 1;
|
||||||
}, file);
|
}, label);
|
||||||
|
|
||||||
if (!queryResult.empty()) {
|
if (!queryResult.empty()) {
|
||||||
return queryResult.length.to!long;
|
return queryResult.length.to!long;
|
||||||
|
|
|
@ -1,51 +1,19 @@
|
||||||
auto _scheme = [
|
auto _scheme = [
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
-- Таблица users
|
-- Таблица labels
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS labels (
|
||||||
-- 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,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
-- имя процесса
|
-- имя метки
|
||||||
name TEXT NOT NULL UNIQUE
|
name TEXT NOT NULL UNIQUE
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс по имени процесса
|
-- Индекс по имени метки
|
||||||
CREATE INDEX IF NOT EXISTS idx_processes_name
|
CREATE INDEX IF NOT EXISTS idx_labels_name
|
||||||
ON processes(name)
|
ON labels(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_files_name
|
|
||||||
ON files(name)
|
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
@ -54,8 +22,8 @@ auto _scheme = [
|
||||||
CREATE TABLE IF NOT EXISTS snapshots (
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
-- идентификатор снимка
|
-- идентификатор снимка
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
-- Файл
|
-- метка/название снимка
|
||||||
file INTEGER NOT NULL,
|
label INTEGER NOT NULL,
|
||||||
-- SHA-256 всего файла (BLOB(32))
|
-- SHA-256 всего файла (BLOB(32))
|
||||||
sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
|
sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
|
||||||
-- Комментарий/описание
|
-- Комментарий/описание
|
||||||
|
@ -64,12 +32,6 @@ auto _scheme = [
|
||||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
-- длина исходного файла в байтах
|
-- длина исходного файла в байтах
|
||||||
source_length INTEGER NOT NULL,
|
source_length INTEGER NOT NULL,
|
||||||
-- UID пользователя (эффективный)
|
|
||||||
uid INTEGER NOT NULL,
|
|
||||||
-- RUID пользователя (реальный)
|
|
||||||
ruid INTEGER NOT NULL,
|
|
||||||
-- Процесс
|
|
||||||
process INTEGER NOT NULL,
|
|
||||||
-- FastCDC: минимальный размер чанка
|
-- FastCDC: минимальный размер чанка
|
||||||
algo_min INTEGER NOT NULL,
|
algo_min INTEGER NOT NULL,
|
||||||
-- FastCDC: целевой размер чанка
|
-- FastCDC: целевой размер чанка
|
||||||
|
@ -83,21 +45,8 @@ auto _scheme = [
|
||||||
-- 0=pending, 1=ready
|
-- 0=pending, 1=ready
|
||||||
status INTEGER NOT NULL DEFAULT 0
|
status INTEGER NOT NULL DEFAULT 0
|
||||||
CHECK (status IN (0,1)),
|
CHECK (status IN (0,1)),
|
||||||
-- Внешние ключи
|
FOREIGN KEY (label)
|
||||||
FOREIGN KEY (uid)
|
REFERENCES labels(id)
|
||||||
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 UPDATE CASCADE
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
|
@ -159,9 +108,9 @@ auto _scheme = [
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для запросов вида: WHERE file=? AND sha256=?
|
-- Индекс для запросов вида: WHERE label=? AND sha256=?
|
||||||
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
||||||
ON snapshots(file, sha256)
|
ON snapshots(label, sha256)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для обратного поиска использования blob по sha256
|
-- Индекс для обратного поиска использования blob по sha256
|
||||||
|
@ -286,14 +235,11 @@ auto _scheme = [
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
WHEN
|
WHEN
|
||||||
NEW.id IS NOT OLD.id OR
|
NEW.id IS NOT OLD.id OR
|
||||||
NEW.file IS NOT OLD.file OR
|
NEW.label IS NOT OLD.label OR
|
||||||
NEW.sha256 IS NOT OLD.sha256 OR
|
NEW.sha256 IS NOT OLD.sha256 OR
|
||||||
NEW.description IS NOT OLD.description OR
|
NEW.description IS NOT OLD.description OR
|
||||||
NEW.created_utc IS NOT OLD.created_utc OR
|
NEW.created_utc IS NOT OLD.created_utc OR
|
||||||
NEW.source_length IS NOT OLD.source_length 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_min IS NOT OLD.algo_min OR
|
||||||
NEW.algo_normal IS NOT OLD.algo_normal OR
|
NEW.algo_normal IS NOT OLD.algo_normal OR
|
||||||
NEW.algo_max IS NOT OLD.algo_max OR
|
NEW.algo_max IS NOT OLD.algo_max OR
|
||||||
|
@ -326,16 +272,16 @@ auto _scheme = [
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
-- Удаление записи из files, если удалён последний snapshot
|
-- Удаление записи из labels, если удалён последний snapshot
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_file
|
CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_label
|
||||||
AFTER DELETE ON snapshots
|
AFTER DELETE ON snapshots
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM files
|
DELETE FROM labels
|
||||||
WHERE id = OLD.file
|
WHERE id = OLD.label
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM snapshots WHERE file = OLD.file
|
SELECT 1 FROM snapshots WHERE label = OLD.label
|
||||||
);
|
);
|
||||||
END;
|
END;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,26 +9,26 @@ import std.datetime : DateTime;
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Чтение снимка и управление его жизненным циклом.
|
* Snapshot reader and lifecycle helper.
|
||||||
*
|
*
|
||||||
* Класс собирает полный файл из чанков, хранящихся через `DBLite`,
|
* This class reconstructs full file content from chunked storage persisted
|
||||||
* проверяет целостность (SHA-256 каждого чанка и итогового файла)
|
* via `DBLite`, verifies integrity (per-chunk SHA-256 and final file hash),
|
||||||
* и предоставляет безопасное удаление записи о снимке.
|
* and provides a safe way to remove a snapshot record.
|
||||||
*
|
*
|
||||||
* Пример:
|
* Usage:
|
||||||
* ---
|
* ---
|
||||||
* auto s1 = new Snapshot(db, snapshotId);
|
* auto s1 = new Snapshot(db, snapshotId);
|
||||||
* auto bytes = s1.data(); // материализовать весь контент в память
|
* auto bytes = s1.data(); // materialize full content in memory
|
||||||
*
|
*
|
||||||
* // или потоково, без крупной аллокации:
|
* // or stream into a sink to avoid large allocations:
|
||||||
* s1.data((const(ubyte)[] part) {
|
* s1.data((const(ubyte)[] part) {
|
||||||
* // обработать part
|
* // consume part
|
||||||
* });
|
* });
|
||||||
* ---
|
* ---
|
||||||
*
|
*
|
||||||
* Заметки:
|
* Notes:
|
||||||
* - Все проверки целостности обязательны; любое расхождение вызывает исключение.
|
* - All integrity checks are enforced; any mismatch throws.
|
||||||
* - Для очень больших файлов предпочтительнее потоковый вариант с делегатом.
|
* - `data(void delegate(...))` is preferred for very large files.
|
||||||
*/
|
*/
|
||||||
final class Snapshot
|
final class Snapshot
|
||||||
{
|
{
|
||||||
|
@ -36,57 +36,55 @@ private:
|
||||||
DBLite _db;
|
DBLite _db;
|
||||||
DBSnapshot _snapshot;
|
DBSnapshot _snapshot;
|
||||||
|
|
||||||
/// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша.
|
|
||||||
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
|
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
|
||||||
{
|
{
|
||||||
ubyte[] bytes;
|
ubyte[] bytes;
|
||||||
if (chunk.zstd)
|
if (chunk.zstd)
|
||||||
{
|
{
|
||||||
enforce(chunk.zSize == chunk.content.length,
|
enforce(chunk.zSize == chunk.content.length, "Compressed chunk size does not match the expected value");
|
||||||
"Размер сжатого чанка не совпадает с ожидаемым значением");
|
|
||||||
bytes = cast(ubyte[]) uncompress(chunk.content);
|
bytes = cast(ubyte[]) uncompress(chunk.content);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bytes = chunk.content.dup;
|
bytes = chunk.content.dup;
|
||||||
}
|
}
|
||||||
enforce(chunk.size == bytes.length, "Исходный размер чанка не совпадает с ожидаемым значением");
|
enforce(chunk.size == bytes.length, "Original size does not match the expected value");
|
||||||
enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш чанка не совпадает");
|
enforce(chunk.sha256 == digest!SHA256(bytes), "Chunk hash does not match");
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Создать `Snapshot` из уже загруженной строки `DBSnapshot`.
|
/// Construct a `Snapshot` from an already fetched `DBSnapshot` row.
|
||||||
///
|
///
|
||||||
/// Параметры:
|
/// Params:
|
||||||
/// dblite = хэндл базы данных
|
/// dblite = database handle
|
||||||
/// dbSnapshot = метаданные снимка, полученные ранее
|
/// dbSnapshot = snapshot row (metadata) previously retrieved
|
||||||
this(DBLite dblite, DBSnapshot dbSnapshot)
|
this(DBLite dblite, DBSnapshot dbSnapshot)
|
||||||
{
|
{
|
||||||
_db = dblite;
|
_db = dblite;
|
||||||
_snapshot = dbSnapshot;
|
_snapshot = dbSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Создать `Snapshot`, подгрузив метаданные из базы.
|
/// Construct a `Snapshot` by loading metadata from the database.
|
||||||
///
|
///
|
||||||
/// Параметры:
|
/// Params:
|
||||||
/// dblite = хэндл базы данных
|
/// dblite = database handle
|
||||||
/// idSnapshot = идентификатор снимка
|
/// idSnapshot = snapshot id to load
|
||||||
this(DBLite dblite, long idSnapshot)
|
this(DBLite dblite, long idSnapshot)
|
||||||
{
|
{
|
||||||
_db = dblite;
|
_db = dblite;
|
||||||
_snapshot = _db.getSnapshot(idSnapshot);
|
_snapshot = _db.getSnapshot(idSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Материализует полный контент файла в память.
|
/// Materialize the full file content in memory.
|
||||||
///
|
///
|
||||||
/// Собирает чанки по порядку, проверяет SHA-256 каждого чанка и
|
/// Reassembles all chunks in order, validates each chunk SHA-256 and the
|
||||||
/// итоговый SHA-256 файла (`snapshots.sha256`).
|
/// final file SHA-256 (`snapshots.sha256`).
|
||||||
///
|
///
|
||||||
/// Возвращает: новый буфер `ubyte[]` с полным содержимым.
|
/// Returns: full file content as a newly allocated `ubyte[]`
|
||||||
///
|
///
|
||||||
/// Бросает: Exception при любой ошибке целостности.
|
/// Throws: Exception on any integrity check failure
|
||||||
ubyte[] data()
|
ubyte[] data()
|
||||||
{
|
{
|
||||||
auto chunks = _db.getChunks(_snapshot.id);
|
auto chunks = _db.getChunks(_snapshot.id);
|
||||||
|
@ -102,20 +100,20 @@ public:
|
||||||
fctx.put(bytes);
|
fctx.put(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
enforce(_snapshot.sha256 == fctx.finish(), "File hash does not match");
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Потоково передаёт содержимое файла в заданный приёмник.
|
/// Stream the full file content into a caller-provided sink.
|
||||||
///
|
///
|
||||||
/// Избегает одной большой аллокации: чанк декодируется, проверяется
|
/// This variant avoids allocating a single large buffer. Chunks are
|
||||||
/// и передаётся в `sink` по порядку.
|
/// decoded, verified, and passed to `sink` in order.
|
||||||
///
|
///
|
||||||
/// Параметры:
|
/// Params:
|
||||||
/// sink = делегат, вызываемый для каждого проверенного чанка.
|
/// sink = delegate invoked for each verified chunk (may be called many times)
|
||||||
///
|
///
|
||||||
/// Бросает: Exception при любой ошибке целостности.
|
/// Throws: Exception on any integrity check failure
|
||||||
void data(void delegate(const(ubyte)[]) sink)
|
void data(void delegate(const(ubyte)[]) sink)
|
||||||
{
|
{
|
||||||
auto chunks = _db.getChunks(_snapshot.id);
|
auto chunks = _db.getChunks(_snapshot.id);
|
||||||
|
@ -128,17 +126,17 @@ public:
|
||||||
fctx.put(bytes);
|
fctx.put(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
enforce(_snapshot.sha256 == fctx.finish(), "File hash does not match");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет снимок из базы в транзакции.
|
/// Remove this snapshot from the database inside a transaction.
|
||||||
///
|
///
|
||||||
/// Открывает транзакцию IMMEDIATE, удаляет запись о снимке и коммитит.
|
/// Starts an IMMEDIATE transaction, deletes the snapshot row, and commits.
|
||||||
/// В случае ошибки откатывает.
|
/// On any failure it rolls back.
|
||||||
///
|
///
|
||||||
/// Возвращает: `true`, если запись была удалена.
|
/// Returns: `true` if the snapshot row was deleted, `false` otherwise
|
||||||
///
|
///
|
||||||
/// Примечание: не выполняет сборку мусора по блобам.
|
/// Note: Does not garbage-collect unreferenced blobs; perform that separately.
|
||||||
bool remove()
|
bool remove()
|
||||||
{
|
{
|
||||||
_db.beginImmediate();
|
_db.beginImmediate();
|
||||||
|
@ -162,41 +160,37 @@ public:
|
||||||
return _snapshot.id == idDeleted;
|
return _snapshot.id == idDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------
|
/// Snapshot id (primary key).
|
||||||
// Доступ к метаданным снимка
|
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
/// ID снимка (PRIMARY KEY).
|
|
||||||
@property long id() const nothrow @safe
|
@property long id() const nothrow @safe
|
||||||
{
|
{
|
||||||
return _snapshot.id;
|
return _snapshot.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Имя файла (из таблицы `files`).
|
/// User-defined label.
|
||||||
@property string file() const nothrow @safe
|
@property string label() const @safe
|
||||||
{
|
{
|
||||||
return _snapshot.file;
|
return _snapshot.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Время создания (UTC).
|
/// Creation timestamp (UTC) from the database.
|
||||||
@property DateTime created() const @safe
|
@property DateTime created() const @safe
|
||||||
{
|
{
|
||||||
return _snapshot.createdUtc;
|
return _snapshot.createdUtc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Длина исходного файла (байты).
|
/// Original file length in bytes.
|
||||||
@property long length() const nothrow @safe
|
@property long length() const nothrow @safe
|
||||||
{
|
{
|
||||||
return _snapshot.sourceLength;
|
return _snapshot.sourceLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ожидаемый SHA-256 всего файла (сырые 32 байта).
|
/// Expected SHA-256 of the full file (32 raw bytes).
|
||||||
@property ubyte[32] sha256() const nothrow @safe
|
@property ubyte[32] sha256() const nothrow @safe
|
||||||
{
|
{
|
||||||
return _snapshot.sha256;
|
return _snapshot.sha256;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Статус снимка (строкой).
|
/// Snapshot status as a string (enum to string).
|
||||||
@property string status() const
|
@property string status() const
|
||||||
{
|
{
|
||||||
import std.conv : to;
|
import std.conv : to;
|
||||||
|
@ -204,81 +198,9 @@ public:
|
||||||
return _snapshot.status.to!string;
|
return _snapshot.status.to!string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Необязательное описание.
|
/// Optional human-readable description.
|
||||||
@property string description() const nothrow @safe
|
@property string description() const nothrow @safe
|
||||||
{
|
{
|
||||||
return _snapshot.description;
|
return _snapshot.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FastCDC: минимальный размер чанка.
|
|
||||||
@property long algoMin() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.algoMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FastCDC: целевой (нормальный) размер чанка.
|
|
||||||
@property long algoNormal() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.algoNormal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FastCDC: максимальный размер чанка.
|
|
||||||
@property long algoMax() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.algoMax;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FastCDC: строгая маска.
|
|
||||||
@property long maskS() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.maskS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// FastCDC: слабая маска.
|
|
||||||
@property long maskL() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.maskL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UID процесса (effective).
|
|
||||||
@property long uid() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Real UID процесса.
|
|
||||||
@property long ruid() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.ruid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Имя пользователя для `uid`.
|
|
||||||
@property string uidName() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.uidName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Имя пользователя для `ruid`.
|
|
||||||
@property string ruidName() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.ruidName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Имя процесса (из таблицы `processes`).
|
|
||||||
@property string process() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.process;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Удобный флаг: снимок «готов».
|
|
||||||
@property bool isReady() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.status == SnapshotStatus.ready;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Удобный флаг: снимок «в процессе».
|
|
||||||
@property bool isPending() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.status == SnapshotStatus.pending;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,45 +6,35 @@ import cdcdb.snapshot;
|
||||||
|
|
||||||
import zstd : compress, Level;
|
import zstd : compress, Level;
|
||||||
|
|
||||||
/// Контекст создания снимка (идентификаторы и процесс).
|
|
||||||
struct Context
|
|
||||||
{
|
|
||||||
long uid; /// UID процесса (effective).
|
|
||||||
long ruid; /// Real UID процесса.
|
|
||||||
string uidName; /// Имя пользователя для UID.
|
|
||||||
string ruidName; /// Имя пользователя для RUID.
|
|
||||||
string process; /// Имя процесса.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Высокоуровневый фасад для хранения: разбивает данные на чанки CDC,
|
* High-level storage facade: splits data into CDC chunks, stores chunks/blobs
|
||||||
* сохраняет чанки/блобы в SQLite через `DBLite`, связывает их в снимки
|
* into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot`
|
||||||
* и возвращает объекты `Snapshot` для последующего чтения и удаления.
|
* objects for retrieval and deletion.
|
||||||
*
|
*
|
||||||
* Возможности:
|
* Features:
|
||||||
* - Разбиение FastCDC (контентно-зависимое, настраиваемые размеры/маски).
|
* - FastCDC-based content-defined chunking (configurable sizes/masks)
|
||||||
* - Опциональное сжатие Zstandard (уровень задаётся).
|
* - Optional Zstandard compression (level configurable)
|
||||||
* - Идемпотентное создание снимков: пропускает, если последний снимок совпадает.
|
* - Idempotent snapshot creation: skips if identical to the latest for label
|
||||||
*
|
*
|
||||||
* Типичное использование:
|
* Typical usage:
|
||||||
* ---
|
* ---
|
||||||
* auto store = new Storage("base.db", true, Level.max);
|
* auto store = new Storage("cdc.sqlite", true, Level.default_);
|
||||||
* store.setupCDC(4096, 8192, 16384, 0x3FFF, 0x03FF);
|
* 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"); // удалить по имени файла
|
* auto snap = store.newSnapshot("my.txt", data, "initial import");
|
||||||
|
* auto bytes = snap.data(); // retrieve
|
||||||
|
*
|
||||||
|
* auto removed = store.removeSnapshots("my.txt"); // remove by label
|
||||||
* ---
|
* ---
|
||||||
*/
|
*/
|
||||||
final class Storage
|
final class Storage
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
// Параметры БД
|
// Database parameters
|
||||||
DBLite _db;
|
DBLite _db;
|
||||||
bool _zstd;
|
bool _zstd;
|
||||||
int _level;
|
int _level;
|
||||||
// Настройки CDC
|
// CDC settings
|
||||||
CDC _cdc;
|
CDC _cdc;
|
||||||
size_t _minSize;
|
size_t _minSize;
|
||||||
size_t _normalSize;
|
size_t _normalSize;
|
||||||
|
@ -52,7 +42,6 @@ private:
|
||||||
size_t _maskS;
|
size_t _maskS;
|
||||||
size_t _maskL;
|
size_t _maskL;
|
||||||
|
|
||||||
/// Инициализация параметров FastCDC.
|
|
||||||
void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024,
|
void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024,
|
||||||
size_t maskS = 0xFF, size_t maskL = 0x0F)
|
size_t maskS = 0xFF, size_t maskL = 0x0F)
|
||||||
{
|
{
|
||||||
|
@ -61,21 +50,20 @@ private:
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
_maskS = maskS;
|
_maskS = maskS;
|
||||||
_maskL = maskL;
|
_maskL = maskL;
|
||||||
// CDC не хранит динамического состояния, переинициализация безопасна
|
// CDC holds no dynamically allocated state; reinitialization is safe
|
||||||
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
|
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Конструктор: открывает/создаёт БД и подготавливает фасад.
|
/// Construct the storage facade and open (or create) the database.
|
||||||
///
|
///
|
||||||
/// Параметры:
|
/// Params:
|
||||||
/// database = путь к файлу SQLite
|
/// database = path to SQLite file
|
||||||
/// zstd = включить Zstd-сжатие для блобов
|
/// zstd = enable Zstandard compression for stored blobs
|
||||||
/// level = уровень сжатия (см. `zstd.Level`)
|
/// level = Zstd compression level (see `zstd.Level`)
|
||||||
/// busyTimeout = таймаут ожидания блокировки SQLite (мс)
|
/// busyTimeout = SQLite busy timeout in milliseconds
|
||||||
/// maxRetries = число повторов при SQLITE_BUSY/LOCKED
|
/// maxRetries = max retries on SQLITE_BUSY/LOCKED errors
|
||||||
this(string database, bool zstd = false, int level = Level.base,
|
this(string database, bool zstd = false, int level = Level.base, size_t busyTimeout = 3000, size_t maxRetries = 3)
|
||||||
size_t busyTimeout = 3000, size_t maxRetries = 3)
|
|
||||||
{
|
{
|
||||||
_db = new DBLite(database, busyTimeout, maxRetries);
|
_db = new DBLite(database, busyTimeout, maxRetries);
|
||||||
_zstd = zstd;
|
_zstd = zstd;
|
||||||
|
@ -83,46 +71,49 @@ public:
|
||||||
initCDC();
|
initCDC();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Перенастроить параметры CDC (влияет на будущие снимки).
|
/// Reconfigure CDC parameters (takes effect for subsequent snapshots).
|
||||||
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize,
|
///
|
||||||
size_t maskS, size_t maskL)
|
/// Params:
|
||||||
|
/// minSize, normalSize, maxSize, maskS, maskL = FastCDC parameters
|
||||||
|
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, size_t maskS, size_t maskL)
|
||||||
{
|
{
|
||||||
initCDC(minSize, normalSize, maxSize, maskS, maskL);
|
initCDC(minSize, normalSize, maxSize, maskS, maskL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Создаёт новый снимок из массива байт.
|
/// Create a new snapshot from raw data.
|
||||||
///
|
///
|
||||||
/// - Разбивает данные по текущим параметрам FastCDC.
|
/// - Splits data with FastCDC using current settings.
|
||||||
/// - Опционально сжимает чанки Zstd.
|
/// - Optionally compresses chunks with Zstd.
|
||||||
/// - Сохраняет уникальные блобы и связывает их со снимком.
|
/// - Stores unique blobs and links them to the created snapshot.
|
||||||
/// - Если последний снимок для файла совпадает по SHA-256, возвращает `null`.
|
/// - If the latest snapshot for `label` already has the same file SHA-256,
|
||||||
|
/// returns `null` (idempotent).
|
||||||
///
|
///
|
||||||
/// Параметры:
|
/// Params:
|
||||||
/// file = имя файла (метка снимка)
|
/// label = user-provided snapshot label (file identifier)
|
||||||
/// data = содержимое файла
|
/// data = raw file bytes
|
||||||
/// context = контекст (uid, ruid, процесс и т.д.)
|
/// description = optional human-readable description
|
||||||
/// description = необязательное описание
|
|
||||||
///
|
///
|
||||||
/// Возвращает: объект `Snapshot` или `null`
|
/// Returns: a `Snapshot` instance for the created snapshot, or `null`
|
||||||
///
|
///
|
||||||
/// Исключения: при пустых данных или ошибках базы
|
/// Throws:
|
||||||
Snapshot newSnapshot(string file, const(ubyte)[] data, Context context,
|
/// Exception if `data` is empty or on database/storage errors
|
||||||
string description = string.init)
|
Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init)
|
||||||
{
|
{
|
||||||
if (data.length == 0)
|
if (data.length == 0)
|
||||||
{
|
{
|
||||||
throw new Exception("Данные имеют нулевую длину");
|
throw new Exception("Data has zero length");
|
||||||
}
|
}
|
||||||
|
|
||||||
import std.digest.sha : SHA256, digest;
|
import std.digest.sha : SHA256, digest;
|
||||||
|
|
||||||
ubyte[32] sha256 = digest!SHA256(data);
|
ubyte[32] sha256 = digest!SHA256(data);
|
||||||
|
|
||||||
// Если последний снимок совпадает — пропустить
|
// If the last snapshot for the label matches current content
|
||||||
if (_db.isLast(file, sha256))
|
if (_db.isLast(label, sha256))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
_db.beginImmediate();
|
_db.beginImmediate();
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
|
|
||||||
scope (exit)
|
scope (exit)
|
||||||
|
@ -135,18 +126,11 @@ public:
|
||||||
_db.commit();
|
_db.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запись пользователей/файлов/процессов
|
_db.addLabel(label);
|
||||||
_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 dbSnapshot;
|
||||||
dbSnapshot.file = file;
|
|
||||||
|
dbSnapshot.label = label;
|
||||||
dbSnapshot.sha256 = sha256;
|
dbSnapshot.sha256 = sha256;
|
||||||
dbSnapshot.description = description;
|
dbSnapshot.description = description;
|
||||||
dbSnapshot.sourceLength = data.length;
|
dbSnapshot.sourceLength = data.length;
|
||||||
|
@ -155,19 +139,18 @@ public:
|
||||||
dbSnapshot.algoMax = _maxSize;
|
dbSnapshot.algoMax = _maxSize;
|
||||||
dbSnapshot.maskS = _maskS;
|
dbSnapshot.maskS = _maskS;
|
||||||
dbSnapshot.maskL = _maskL;
|
dbSnapshot.maskL = _maskL;
|
||||||
dbSnapshot.uid = context.uid;
|
|
||||||
dbSnapshot.ruid = context.ruid;
|
|
||||||
dbSnapshot.process = context.process;
|
|
||||||
|
|
||||||
auto idSnapshot = _db.addSnapshot(dbSnapshot);
|
auto idSnapshot = _db.addSnapshot(dbSnapshot);
|
||||||
|
|
||||||
// Чанки и блобы
|
|
||||||
DBSnapshotChunk dbSnapshotChunk;
|
DBSnapshotChunk dbSnapshotChunk;
|
||||||
DBBlob dbBlob;
|
DBBlob dbBlob;
|
||||||
|
|
||||||
dbBlob.zstd = _zstd;
|
dbBlob.zstd = _zstd;
|
||||||
|
|
||||||
|
// Split into chunks
|
||||||
Chunk[] chunks = _cdc.split(data);
|
Chunk[] chunks = _cdc.split(data);
|
||||||
|
|
||||||
|
// Write chunks to DB
|
||||||
foreach (chunk; chunks)
|
foreach (chunk; chunks)
|
||||||
{
|
{
|
||||||
dbBlob.sha256 = chunk.sha256;
|
dbBlob.sha256 = chunk.sha256;
|
||||||
|
@ -175,8 +158,7 @@ public:
|
||||||
|
|
||||||
auto content = data[chunk.offset .. chunk.offset + chunk.size];
|
auto content = data[chunk.offset .. chunk.offset + chunk.size];
|
||||||
|
|
||||||
if (_zstd)
|
if (_zstd) {
|
||||||
{
|
|
||||||
ubyte[] zBytes = compress(content, _level);
|
ubyte[] zBytes = compress(content, _level);
|
||||||
size_t zSize = zBytes.length;
|
size_t zSize = zBytes.length;
|
||||||
ubyte[32] zHash = digest!SHA256(zBytes);
|
ubyte[32] zHash = digest!SHA256(zBytes);
|
||||||
|
@ -184,12 +166,11 @@ public:
|
||||||
dbBlob.zSize = zSize;
|
dbBlob.zSize = zSize;
|
||||||
dbBlob.zSha256 = zHash;
|
dbBlob.zSha256 = zHash;
|
||||||
dbBlob.content = zBytes;
|
dbBlob.content = zBytes;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
dbBlob.content = content.dup;
|
dbBlob.content = content.dup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store/ensure blob
|
||||||
_db.addBlob(dbBlob);
|
_db.addBlob(dbBlob);
|
||||||
|
|
||||||
dbSnapshotChunk.snapshotId = idSnapshot;
|
dbSnapshotChunk.snapshotId = idSnapshot;
|
||||||
|
@ -197,49 +178,76 @@ public:
|
||||||
dbSnapshotChunk.offset = chunk.offset;
|
dbSnapshotChunk.offset = chunk.offset;
|
||||||
dbSnapshotChunk.sha256 = chunk.sha256;
|
dbSnapshotChunk.sha256 = chunk.sha256;
|
||||||
|
|
||||||
|
// Link chunk to snapshot
|
||||||
_db.addSnapshotChunk(dbSnapshotChunk);
|
_db.addSnapshotChunk(dbSnapshotChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true;
|
ok = true;
|
||||||
return new Snapshot(_db, idSnapshot);
|
|
||||||
|
Snapshot snapshot = new Snapshot(_db, idSnapshot);
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет все снимки по имени файла.
|
/// Delete snapshots by label.
|
||||||
long removeSnapshots(string file)
|
///
|
||||||
{
|
/// Params:
|
||||||
return _db.deleteSnapshots(file);
|
/// label = snapshot label
|
||||||
|
///
|
||||||
|
/// Returns: number of deleted snapshots
|
||||||
|
long removeSnapshots(string label) {
|
||||||
|
return _db.deleteSnapshot(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет конкретный снимок по объекту `Snapshot`.
|
/// Delete a specific snapshot instance.
|
||||||
bool removeSnapshot(Snapshot snapshot)
|
///
|
||||||
{
|
/// Params:
|
||||||
return removeSnapshot(snapshot.id);
|
/// snapshot = `Snapshot` to remove
|
||||||
|
///
|
||||||
|
/// Returns: `true` on success, `false` otherwise
|
||||||
|
bool removeSnapshots(Snapshot snapshot) {
|
||||||
|
return removeSnapshots(snapshot.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет снимок по id.
|
/// Delete a snapshot by id.
|
||||||
bool removeSnapshot(long idSnapshot)
|
///
|
||||||
{
|
/// Params:
|
||||||
|
/// idSnapshot = snapshot id
|
||||||
|
///
|
||||||
|
/// Returns: `true` if the row was deleted
|
||||||
|
bool removeSnapshots(long idSnapshot) {
|
||||||
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
|
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает `Snapshot` по id.
|
/// Get a `Snapshot` object by id.
|
||||||
Snapshot getSnapshot(long idSnapshot)
|
///
|
||||||
{
|
/// Params:
|
||||||
|
/// idSnapshot = snapshot id
|
||||||
|
///
|
||||||
|
/// Returns: `Snapshot` handle (metadata loaded lazily via constructor)
|
||||||
|
Snapshot getSnapshot(long idSnapshot) {
|
||||||
return new Snapshot(_db, idSnapshot);
|
return new Snapshot(_db, idSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Возвращает список снимков (опционально фильтр по имени файла).
|
/// List snapshots (optionally filtered by label).
|
||||||
Snapshot[] getSnapshots(string file = string.init)
|
///
|
||||||
{
|
/// Params:
|
||||||
|
/// label = filter by exact label; empty string returns all
|
||||||
|
///
|
||||||
|
/// Returns: array of `Snapshot` handles
|
||||||
|
Snapshot[] getSnapshots(string label = string.init) {
|
||||||
Snapshot[] snapshots;
|
Snapshot[] snapshots;
|
||||||
foreach (snapshot; _db.getSnapshots(file))
|
|
||||||
{
|
foreach (snapshot; _db.getSnapshots(label)) {
|
||||||
snapshots ~= new Snapshot(_db, snapshot);
|
snapshots ~= new Snapshot(_db, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Версия библиотеки.
|
/// Library version string.
|
||||||
|
///
|
||||||
|
/// Returns: semantic version of the `cdcdb` library
|
||||||
string getVersion() const @safe nothrow
|
string getVersion() const @safe nothrow
|
||||||
{
|
{
|
||||||
import cdcdb.version_ : cdcdbVersion;
|
import cdcdb.version_ : cdcdbVersion;
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
module cdcdb.version_;
|
module cdcdb.version_;
|
||||||
|
|
||||||
enum cdcdbVersion = "0.1.0";
|
enum cdcdbVersion = "0.1.1";
|
||||||
|
|
18
test/app.d
18
test/app.d
|
@ -8,18 +8,6 @@ void main()
|
||||||
// Создаем временную базу для примера
|
// Создаем временную базу для примера
|
||||||
string dbPath = "./bin/example.db";
|
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
|
// Инициализация Storage с компрессией Zstd
|
||||||
auto storage = new Storage(dbPath, true, Level.speed);
|
auto storage = new Storage(dbPath, true, Level.speed);
|
||||||
|
|
||||||
|
@ -31,17 +19,17 @@ void main()
|
||||||
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
|
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
|
||||||
|
|
||||||
// Создание первого снимка
|
// Создание первого снимка
|
||||||
auto snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
|
auto snap1 = storage.newSnapshot("example_file", data1, "Версия 1.0");
|
||||||
if (snap1)
|
if (snap1)
|
||||||
{
|
{
|
||||||
writeln("Создан снимок с ID: ", snap1.id);
|
writeln("Создан снимок с ID: ", snap1.id);
|
||||||
writeln("Файл: ", snap1.file);
|
writeln("Метка: ", snap1.label);
|
||||||
writeln("Размер: ", snap1.length, " байт");
|
writeln("Размер: ", snap1.length, " байт");
|
||||||
writeln("Статус: ", snap1.status);
|
writeln("Статус: ", snap1.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание второго снимка (обновление)
|
// Создание второго снимка (обновление)
|
||||||
auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
|
auto snap2 = storage.newSnapshot("example_file", data2, "Версия 2.0");
|
||||||
if (snap2)
|
if (snap2)
|
||||||
{
|
{
|
||||||
writeln("Создан обновленный снимок с ID: ", snap2.id);
|
writeln("Создан обновленный снимок с ID: ", snap2.id);
|
||||||
|
|
72
test/unittest.d
Normal file
72
test/unittest.d
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import cdcdb;
|
||||||
|
|
||||||
|
import std.file : read, write, remove, exists;
|
||||||
|
import std.path : buildPath;
|
||||||
|
import std.digest.sha : digest, SHA256;
|
||||||
|
import std.exception : assertThrown, assertNotThrown;
|
||||||
|
import std.datetime : DateTime;
|
||||||
|
import core.thread : Thread;
|
||||||
|
import core.time : msecs, seconds;
|
||||||
|
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
const string dbPath = "./bin/test_cdcdb.db";
|
||||||
|
|
||||||
|
if (exists(dbPath)) {
|
||||||
|
remove(dbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тест конструктора Storage
|
||||||
|
auto storage = new Storage(dbPath, true, 22);
|
||||||
|
|
||||||
|
// Тест настройки CDC
|
||||||
|
storage.setupCDC(128, 256, 512, 0xFF, 0x0F);
|
||||||
|
|
||||||
|
// Тест создания снимка
|
||||||
|
ubyte[] data1 = cast(ubyte[]) "Hello, World!".dup;
|
||||||
|
auto snap1 = storage.newSnapshot("test_label", data1, "First snapshot");
|
||||||
|
assert(snap1 !is null);
|
||||||
|
assert(snap1.label == "test_label");
|
||||||
|
assert(snap1.length == data1.length);
|
||||||
|
assert(snap1.sha256 == digest!SHA256(data1));
|
||||||
|
assert(snap1.status == "ready");
|
||||||
|
assert(snap1.description == "First snapshot");
|
||||||
|
|
||||||
|
// Тест дубликата (не должен создать новый)
|
||||||
|
auto snapDup = storage.newSnapshot("test_label", data1);
|
||||||
|
assert(snapDup is null);
|
||||||
|
|
||||||
|
// Тест изменения данных
|
||||||
|
ubyte[] data2 = cast(ubyte[]) "Hello, Changed World!".dup;
|
||||||
|
auto snap2 = storage.newSnapshot("test_label", data2);
|
||||||
|
assert(snap2 !is null);
|
||||||
|
assert(snap2.sha256 == digest!SHA256(data2));
|
||||||
|
|
||||||
|
// Тест восстановления данных
|
||||||
|
auto restored = snap1.data();
|
||||||
|
assert(restored == data1);
|
||||||
|
bool streamedOk = false;
|
||||||
|
snap2.data((const(ubyte)[] chunk) {
|
||||||
|
assert(chunk == data2); // Поскольку маленький файл — один фрагмент
|
||||||
|
streamedOk = true;
|
||||||
|
});
|
||||||
|
assert(streamedOk);
|
||||||
|
|
||||||
|
// Тест getSnapshots
|
||||||
|
auto snaps = storage.getSnapshots("test_label");
|
||||||
|
assert(snaps.length == 2);
|
||||||
|
assert(snaps[0].id == snap1.id);
|
||||||
|
assert(snaps[1].id == snap2.id);
|
||||||
|
|
||||||
|
auto allSnaps = storage.getSnapshots();
|
||||||
|
assert(allSnaps.length == 2);
|
||||||
|
|
||||||
|
// Тест удаления
|
||||||
|
assert(snap1.remove());
|
||||||
|
snaps = storage.getSnapshots("test_label");
|
||||||
|
assert(snaps.length == 1);
|
||||||
|
assert(snaps[0].id == snap2.id);
|
||||||
|
|
||||||
|
// Тест пустых данных
|
||||||
|
assertThrown!Exception(storage.newSnapshot("empty", []));
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue