Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f50b21457 | |||
| dd28c4b4d7 | |||
| c290bb6f51 | |||
| f34b26c2b5 | |||
| 49ee7a4053 |
18 changed files with 1242 additions and 792 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,4 +15,4 @@ cdcdb-test-*
|
||||||
*.obj
|
*.obj
|
||||||
*.lst
|
*.lst
|
||||||
bin
|
bin
|
||||||
lib
|
/lib
|
||||||
|
|
|
||||||
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,18 +1,13 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [0.1.1] - 2025-09-14
|
## [0.1.0] — 2025-09-14
|
||||||
### Added
|
### Added
|
||||||
- Table `labels` for snapshot labels.
|
- Библиотека для хранения и управления снимками данных на базе SQLite с контентно-зависимым разбиением (FastCDC).
|
||||||
### Fixed
|
- Дедупликация по SHA-256 чанков, опциональная компрессия Zstd.
|
||||||
- Improved data integrity with label normalization.
|
- Сквозная проверка целостности: хеш каждого чанка и итогового файла.
|
||||||
|
- Поддержка транзакций (WAL), ограничения целостности и триггеры в БД.
|
||||||
## [0.1.0] — 2025-09-13
|
- Высокоуровневый API:
|
||||||
### Added
|
- `Storage`: `newSnapshot`, `getSnapshots`, `getSnapshot`, `removeSnapshots`, `removeSnapshot`, `setupCDC`, `getVersion`.
|
||||||
- SQLite-backed snapshot library with content-defined chunking (FastCDC).
|
- `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`).
|
||||||
- Deduplication by SHA-256 of chunks; optional Zstd compression.
|
- `Context`: структура для передачи UID, RUID, имён пользователей и процесса при создании снимка.
|
||||||
- End-to-end integrity checks: per-chunk hash and final file hash.
|
- Инструмент для генерации Gear-таблицы для FastCDC (`tools/gen.d`).
|
||||||
- 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`).
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# 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,66 +1,42 @@
|
||||||
# cdcdb
|
# cdcdb
|
||||||
|
|
||||||
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.
|
Библиотека для версионирования и хранения данных в SQLite с опорой на content-defined chunking (CDC). Алгоритм **FastCDC** используется для разбиения входного потока на чанки переменного размера, что обеспечивает эффективную дедупликацию и экономию места. Поддерживаются опциональное сжатие **Zstd**, транзакции и проверка целостности на основе **SHA-256**. Дополнительно фиксируется контекст выполнения при создании снимков (UID, RUID, процесс). Основные сценарии применения — резервное копирование и контроль версий файлов.
|
||||||
|
|
||||||
## FastCDC algorithm
|
## Алгоритм FastCDC
|
||||||
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.
|
**FastCDC** — это алгоритм разделения данных на чанки переменной длины, основанный на скользящем хэшировании содержимого. Он использует таблицу **Gear** для вычисления "отпечатков" данных и выбирает точки разреза с учётом минимального, целевого и максимального размеров чанка. Такой подход позволяет эффективно выявлять изменения и сохранять только уникальные части данных.
|
||||||
|
|
||||||
## Core classes
|
## Архитектура и основные компоненты
|
||||||
|
|
||||||
### Storage
|
Библиотека разделена на несколько уровней:
|
||||||
High-level API for the SQLite store and snapshot management.
|
|
||||||
|
|
||||||
- **Constructor**: Initializes a connection to SQLite.
|
- **Storage** — фасад для работы с БД SQLite. Отвечает за разбиение данных (FastCDC), сохранение/удаление снимков, управление блобами и транзакциями.
|
||||||
- **Methods**:
|
- **Snapshot** — объект-ссылка на конкретный снимок. Предоставляет доступ к данным (буфером или потоково), а также к метаданным и операциям удаления.
|
||||||
- `newSnapshot`: Creates a snapshot. Returns a `Snapshot` object or `null` if the data matches the latest snapshot.
|
- **Context** — структура с параметрами окружения (UID, RUID, имена пользователей, процесс), передаваемыми при создании снимка.
|
||||||
- `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";
|
||||||
|
|
||||||
// Initialize Storage with Zstd compression
|
// Storage с Zstd-сжатием
|
||||||
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, "Version 1.0");
|
auto snap = storage.newSnapshot("example_file", data, ctx, "Версия 1.0");
|
||||||
if (snap)
|
if (snap)
|
||||||
{
|
{
|
||||||
writeln("Snapshot created: ID=", snap.id, ", Label=", snap.label);
|
writeln("Создан снимок: ID=", snap.id, ", Файл=", snap.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore data
|
// Восстановление через потоковую запись
|
||||||
auto snapshots = storage.getSnapshots("example_file");
|
auto snapshots = storage.getSnapshots("example_file");
|
||||||
if (snapshots.length > 0)
|
if (snapshots.length > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -68,35 +44,25 @@ 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("Data restored to restored.txt");
|
writeln("Данные восстановлены в restored.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete snapshots
|
// Удаление
|
||||||
long deleted = storage.removeSnapshots("example_file");
|
long deleted = storage.removeSnapshots("example_file");
|
||||||
writeln("Deleted snapshots: ", deleted);
|
writeln("Удалено снимков: ", deleted);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tools
|
## Инструменты
|
||||||
|
|
||||||
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:
|
В директории `tools` есть скрипт на D для генерации таблицы Gear, используемой FastCDC.
|
||||||
|
Чтобы создать новую таблицу:
|
||||||
|
|
||||||
```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
98
README.ru.md
|
|
@ -1,98 +0,0 @@
|
||||||
# cdcdb
|
|
||||||
|
|
||||||
Библиотека для хранения и управления снимками текстовых данных в базе SQLite. Использует механизм content-defined chunking (CDC) на основе алгоритма FastCDC для разделения данных на чанки переменного размера, что обеспечивает эффективную дедупликацию. Поддерживает опциональную компрессию Zstd, транзакции и проверку целостности данных через SHA-256. Основное применение — резервное копирование и версионирование текстовых файлов с минимизацией занимаемого пространства.
|
|
||||||
|
|
||||||
## Алгоритм FastCDC
|
|
||||||
FastCDC — это алгоритм разделения данных на чанки переменного размера, основанный на хэшировании содержимого. Он использует таблицу Gear для вычисления "отпечатков" данных, определяя точки разделения с учетом минимального, целевого и максимального размеров чанка. Это позволяет эффективно выявлять изменения в данных и хранить только уникальные чанки, снижая объем хранилища.
|
|
||||||
|
|
||||||
## Основные классы
|
|
||||||
|
|
||||||
### Storage
|
|
||||||
Класс для работы с базой SQLite и управления снимками.
|
|
||||||
|
|
||||||
- **Конструктор**: Инициализирует подключение к базе SQLite.
|
|
||||||
- **Методы**:
|
|
||||||
- `newSnapshot`: Создает снимок данных. Возвращает объект `Snapshot` или `null`, если данные совпадают с последним снимком.
|
|
||||||
- `getSnapshots`: Получает список снимков (все или по метке). Возвращает массив объектов `Snapshot`.
|
|
||||||
- `getSnapshot`: Получает снимок по ID. Возвращает объект `Snapshot`.
|
|
||||||
- `setupCDC`: Настраивает параметры разделения данных CDC. Ничего не возвращает.
|
|
||||||
- `removeSnapshots`: Удаляет снимки по метке, ID или объекту Snapshot. Возвращает количество удаленных снимков (для метки) или `true`/`false` (для ID или объекта).
|
|
||||||
- `getVersion`: Возвращает строку с версией библиотеки.
|
|
||||||
|
|
||||||
### Snapshot
|
|
||||||
Класс для работы с отдельным снимком данных.
|
|
||||||
|
|
||||||
- **Конструктор**: Создает объект снимка для работы с данными по его ID.
|
|
||||||
- **Методы**:
|
|
||||||
- `data`: Извлекает полные данные снимка. Возвращает массив байтов (`ubyte[]`).
|
|
||||||
- `data`: Извлекает данные снимка потоково через делегат. Ничего не возвращает.
|
|
||||||
- `remove`: Удаляет снимок из базы. Возвращает `true` при успешном удалении, иначе `false`.
|
|
||||||
|
|
||||||
- **Свойства**:
|
|
||||||
- `id`: ID снимка (long).
|
|
||||||
- `label`: Метка снимка (string).
|
|
||||||
- `created`: Временная метка создания (UTC, `DateTime`).
|
|
||||||
- `length`: Длина исходных данных (long).
|
|
||||||
- `sha256`: Хэш SHA-256 данных (ubyte[32]).
|
|
||||||
- `status`: Статус снимка ("pending" или "ready").
|
|
||||||
- `description`: Описание снимка (string).
|
|
||||||
|
|
||||||
## Пример использования
|
|
||||||
```d
|
|
||||||
import cdcdb;
|
|
||||||
|
|
||||||
import std.stdio : writeln, File;
|
|
||||||
import std.file : exists, remove;
|
|
||||||
|
|
||||||
void main()
|
|
||||||
{
|
|
||||||
// Создаем базу
|
|
||||||
string dbPath = "example.db";
|
|
||||||
|
|
||||||
// Инициализация Storage с компрессией Zstd
|
|
||||||
auto storage = new Storage(dbPath, true, 22);
|
|
||||||
|
|
||||||
// Создание снимка
|
|
||||||
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
|
|
||||||
auto snap = storage.newSnapshot("example_file", data, "Версия 1.0");
|
|
||||||
if (snap)
|
|
||||||
{
|
|
||||||
writeln("Создан снимок: ID=", snap.id, ", Метка=", snap.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Восстановление данных
|
|
||||||
auto snapshots = storage.getSnapshots("example_file");
|
|
||||||
if (snapshots.length > 0)
|
|
||||||
{
|
|
||||||
auto lastSnap = snapshots[0];
|
|
||||||
File outFile = File("restored.txt", "wb");
|
|
||||||
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
|
|
||||||
outFile.close();
|
|
||||||
writeln("Данные восстановлены в restored.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаление снимков
|
|
||||||
long deleted = storage.removeSnapshots("example_file");
|
|
||||||
writeln("Удалено снимков: ", deleted);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Инструменты
|
|
||||||
В директории `tools` находится скрипт на D для создания таблицы Gear, используемой в алгоритме FastCDC. Он позволяет генерировать пользовательские таблицы хэшей для настройки разделения данных. Для создания новой таблицы выполните:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x ./tools/gen.d
|
|
||||||
./tools/gen.d > ./source/gear.d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
- **В `dub.json`**:
|
|
||||||
```json
|
|
||||||
"dependencies": {
|
|
||||||
"cdcdb": "~>0.1"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **Сборка**: `dub build`.
|
|
||||||
|
|
||||||
## Лицензия
|
|
||||||
Boost Software License 1.0 (BSL-1.0).
|
|
||||||
|
|
@ -1,24 +1,45 @@
|
||||||
|
/// Модуль базовых структур и алгоритмов 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; // 1..N
|
size_t index;
|
||||||
size_t offset; // offset in the source buffer
|
size_t offset;
|
||||||
size_t size; // chunk size
|
size_t size;
|
||||||
immutable(ubyte)[32] sha256; // hex(SHA-256) of the content
|
immutable(ubyte)[32] sha256;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (должна быть сгенерирована отдельно в "gear.d")
|
||||||
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;
|
||||||
|
|
@ -36,13 +57,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]];
|
||||||
|
|
@ -50,7 +71,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]];
|
||||||
|
|
@ -62,10 +83,21 @@ 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,
|
||||||
"Invalid sizes: require min < normal < max and min > 0");
|
"Некорректные размеры: требуется 0 < min < normal < max");
|
||||||
_minSize = minSize;
|
_minSize = minSize;
|
||||||
_normalSize = normalSize;
|
_normalSize = normalSize;
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
|
|
@ -73,6 +105,12 @@ 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;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
98
source/cdcdb/lib/hash.d
Normal file
98
source/cdcdb/lib/hash.d
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
module cdcdb.lib.hash;
|
||||||
|
|
||||||
|
import std.format : format;
|
||||||
|
|
||||||
|
struct Identifier
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
ubyte[] _data;
|
||||||
|
|
||||||
|
ubyte hxc(ref const char c) const
|
||||||
|
{
|
||||||
|
auto lc = cast(char)(c | 32);
|
||||||
|
if (lc >= '0' && lc <= '9')
|
||||||
|
return cast(ubyte)(lc - '0');
|
||||||
|
if (lc >= 'a' && lc <= 'f')
|
||||||
|
return cast(ubyte)(10 + lc - 'a');
|
||||||
|
throw new Exception("Некорректный символ hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
ubyte[] fromHex(ref const string hash) const
|
||||||
|
{
|
||||||
|
import std.exception : enforce;
|
||||||
|
|
||||||
|
enforce(hash.length > 0, "Hex-строка не может быть пустой.");
|
||||||
|
enforce(hash.length <= 32, "Длина hex-строки не должна превышать 32 символа.");
|
||||||
|
|
||||||
|
size_t byteLen = (hash.length + 1) / 2; // Округление вверх для нечётной длины
|
||||||
|
ubyte[] data = new ubyte[byteLen];
|
||||||
|
|
||||||
|
foreach (i; 0 .. hash.length / 2)
|
||||||
|
{
|
||||||
|
data[i] = cast(ubyte)((hxc(hash[2 * i]) << 4) | hxc(hash[2 * i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.length % 2 != 0)
|
||||||
|
{
|
||||||
|
// Для нечётной длины: последний ниббл в старший разряд, младший = 0
|
||||||
|
data[$ - 1] = cast(ubyte)(hxc(hash[$ - 1]) << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// alias _data this;
|
||||||
|
|
||||||
|
this(const string hex)
|
||||||
|
{
|
||||||
|
_data = fromHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void opAssign(const string hex)
|
||||||
|
{
|
||||||
|
_data = fromHex(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
this(ubyte[] data)
|
||||||
|
{
|
||||||
|
assert(data.length <= 16);
|
||||||
|
_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(ref const ubyte[16] data)
|
||||||
|
{
|
||||||
|
assert(data.length <= 16);
|
||||||
|
_data = data.dup;
|
||||||
|
}
|
||||||
|
|
||||||
|
void opAssign(ubyte[] data)
|
||||||
|
{
|
||||||
|
assert(data.length <= 16);
|
||||||
|
_data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
string toString() const @safe pure
|
||||||
|
{
|
||||||
|
return format("%(%02x%)", _data);
|
||||||
|
}
|
||||||
|
|
||||||
|
string compact(int size = 4) const @safe pure
|
||||||
|
{
|
||||||
|
auto length = _data.length >= size && size > 0 ? size : _data.length;
|
||||||
|
return format("%(%02x%)", _data[0 .. length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ubyte[] data()
|
||||||
|
{
|
||||||
|
return _data;
|
||||||
|
}
|
||||||
|
|
||||||
|
ubyte[] opIndex() {
|
||||||
|
return _data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@trusted pure nothrow @nogc @property bool empty() const {
|
||||||
|
return _data.length == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
source/cdcdb/lib/package.d
Normal file
4
source/cdcdb/lib/package.d
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module cdcdb.lib;
|
||||||
|
|
||||||
|
public import cdcdb.lib.hash;
|
||||||
|
public import cdcdb.lib.uts;
|
||||||
61
source/cdcdb/lib/uts.d
Normal file
61
source/cdcdb/lib/uts.d
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
module cdcdb.lib.uts;
|
||||||
|
|
||||||
|
import std.datetime : SysTime, msecs;
|
||||||
|
|
||||||
|
// 2050-01-01 00:00:00 UTC
|
||||||
|
private enum UTS_LAST_TS = 0x967a7600; // 2524608000L
|
||||||
|
// Extended
|
||||||
|
private enum UTS_LAST_TS_EXT = UTS_LAST_TS * 1_000L;
|
||||||
|
|
||||||
|
// Unix Timestamp с миллисекундами
|
||||||
|
struct UTS
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
long _ts;
|
||||||
|
|
||||||
|
long calc(SysTime systime) {
|
||||||
|
long millis = systime.toUnixTime() * 1000L + systime.fracSecs.total!"msecs";
|
||||||
|
return millis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
this(long ts) {
|
||||||
|
assert(ts < UTS_LAST_TS_EXT);
|
||||||
|
_ts = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(SysTime systime) {
|
||||||
|
_ts = calc(systime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void opAssign(SysTime systime) {
|
||||||
|
_ts = calc(systime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void opAssign(long ts) {
|
||||||
|
assert(ts < UTS_LAST_TS_EXT);
|
||||||
|
_ts = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
string toString() const
|
||||||
|
{
|
||||||
|
import std.format : format;
|
||||||
|
|
||||||
|
string formatStr = "%04d-%02d-%02d %02d:%02d:%02d.%03d";
|
||||||
|
long seconds = _ts / 1_000L;
|
||||||
|
long millis = _ts % 1_000L;
|
||||||
|
auto sysTime = SysTime.fromUnixTime(seconds) + msecs(millis);
|
||||||
|
return format(formatStr,
|
||||||
|
sysTime.year, sysTime.month, sysTime.day,
|
||||||
|
sysTime.hour, sysTime.minute, sysTime.second,
|
||||||
|
sysTime.fracSecs.total!"msecs");
|
||||||
|
}
|
||||||
|
|
||||||
|
@property const(SysTime) sys() const @safe {
|
||||||
|
return SysTime.fromUnixTime(_ts / 1_000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@property long unix() const @safe {
|
||||||
|
return _ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
module cdcdb;
|
module cdcdb;
|
||||||
|
|
||||||
|
public import cdcdb.lib;
|
||||||
public import cdcdb.storage;
|
public import cdcdb.storage;
|
||||||
|
public import cdcdb.storagefile;
|
||||||
public import cdcdb.snapshot;
|
public import cdcdb.snapshot;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,51 @@
|
||||||
auto _scheme = [
|
auto _scheme = [
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
-- Таблица labels
|
-- Таблица users
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS labels (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
-- идентификатор метки
|
-- Linux UID
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
uid INTEGER PRIMARY KEY,
|
||||||
-- имя метки
|
-- текстовое представление пользователя
|
||||||
name TEXT NOT NULL UNIQUE
|
name TEXT NOT NULL UNIQUE
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс по имени метки
|
-- Индекс по имени пользователя
|
||||||
CREATE INDEX IF NOT EXISTS idx_labels_name
|
CREATE INDEX IF NOT EXISTS idx_users_name
|
||||||
ON labels(name)
|
ON users(name)
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Таблица processes
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS processes (
|
||||||
|
-- идентификатор процесса
|
||||||
|
id BLOB PRIMARY KEY,
|
||||||
|
-- имя процесса
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
) WITHOUT ROWID
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- Индекс по имени процесса
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_processes_name
|
||||||
|
ON processes(name)
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- Таблица files
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
|
-- идентификатор файла
|
||||||
|
id BLOB PRIMARY KEY,
|
||||||
|
-- имя файла
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
) WITHOUT ROWID
|
||||||
|
},
|
||||||
|
q{
|
||||||
|
-- Индекс по имени файла
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_name
|
||||||
|
ON files(name)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
|
@ -21,17 +53,23 @@ auto _scheme = [
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS snapshots (
|
CREATE TABLE IF NOT EXISTS snapshots (
|
||||||
-- идентификатор снимка
|
-- идентификатор снимка
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id BLOB PRIMARY KEY,
|
||||||
-- метка/название снимка
|
-- Файл
|
||||||
label INTEGER NOT NULL,
|
file BLOB 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),
|
||||||
-- Комментарий/описание
|
-- Комментарий/описание
|
||||||
description TEXT DEFAULT NULL,
|
description TEXT DEFAULT NULL,
|
||||||
-- время создания (UTC)
|
-- время создания (UTC)
|
||||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
created_utc INTEGER NOT NULL,
|
||||||
-- длина исходного файла в байтах
|
-- длина исходного файла в байтах
|
||||||
source_length INTEGER NOT NULL,
|
source_length INTEGER NOT NULL,
|
||||||
|
-- UID пользователя (эффективный)
|
||||||
|
uid INTEGER NOT NULL,
|
||||||
|
-- RUID пользователя (реальный)
|
||||||
|
ruid INTEGER NOT NULL,
|
||||||
|
-- Процесс
|
||||||
|
process BLOB NOT NULL,
|
||||||
-- FastCDC: минимальный размер чанка
|
-- FastCDC: минимальный размер чанка
|
||||||
algo_min INTEGER NOT NULL,
|
algo_min INTEGER NOT NULL,
|
||||||
-- FastCDC: целевой размер чанка
|
-- FastCDC: целевой размер чанка
|
||||||
|
|
@ -45,11 +83,24 @@ 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)
|
-- Внешние ключи
|
||||||
REFERENCES labels(id)
|
FOREIGN KEY (uid)
|
||||||
|
REFERENCES users(uid)
|
||||||
|
ON UPDATE RESTRICT
|
||||||
|
ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (ruid)
|
||||||
|
REFERENCES users(uid)
|
||||||
|
ON UPDATE RESTRICT
|
||||||
|
ON DELETE RESTRICT,
|
||||||
|
FOREIGN KEY (process)
|
||||||
|
REFERENCES processes(id)
|
||||||
|
ON UPDATE RESTRICT
|
||||||
|
ON DELETE RESTRICT
|
||||||
|
FOREIGN KEY (file)
|
||||||
|
REFERENCES files(id)
|
||||||
ON UPDATE CASCADE
|
ON UPDATE CASCADE
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
)
|
) WITHOUT ROWID
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
|
@ -67,9 +118,9 @@ auto _scheme = [
|
||||||
-- байты (сжатые при zstd=1, иначе исходные)
|
-- байты (сжатые при zstd=1, иначе исходные)
|
||||||
content BLOB NOT NULL,
|
content BLOB NOT NULL,
|
||||||
-- время создания записи (UTC)
|
-- время создания записи (UTC)
|
||||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
created_utc INTEGER NOT NULL,
|
||||||
-- время последней ссылки (UTC)
|
-- время последней ссылки (UTC)
|
||||||
last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
last_seen_utc INTEGER NOT NULL,
|
||||||
-- число ссылок из snapshot_chunks
|
-- число ссылок из snapshot_chunks
|
||||||
refcount INTEGER NOT NULL DEFAULT 0,
|
refcount INTEGER NOT NULL DEFAULT 0,
|
||||||
-- 0=нет сжатия, 1=zstd
|
-- 0=нет сжатия, 1=zstd
|
||||||
|
|
@ -81,7 +132,7 @@ auto _scheme = [
|
||||||
(zstd = 0 AND length(content) = size)
|
(zstd = 0 AND length(content) = size)
|
||||||
),
|
),
|
||||||
CHECK (z_sha256 IS NULL OR length(z_sha256) = 32)
|
CHECK (z_sha256 IS NULL OR length(z_sha256) = 32)
|
||||||
)
|
) WITHOUT ROWID
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
|
|
@ -89,7 +140,7 @@ auto _scheme = [
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS snapshot_chunks (
|
CREATE TABLE IF NOT EXISTS snapshot_chunks (
|
||||||
-- FK -> snapshots.id
|
-- FK -> snapshots.id
|
||||||
snapshot_id INTEGER NOT NULL,
|
snapshot_id BLOB NOT NULL,
|
||||||
-- порядковый номер чанка в снимке
|
-- порядковый номер чанка в снимке
|
||||||
chunk_index INTEGER NOT NULL,
|
chunk_index INTEGER NOT NULL,
|
||||||
-- смещение чанка в исходном файле, байт
|
-- смещение чанка в исходном файле, байт
|
||||||
|
|
@ -105,12 +156,12 @@ auto _scheme = [
|
||||||
REFERENCES blobs(sha256)
|
REFERENCES blobs(sha256)
|
||||||
ON UPDATE RESTRICT
|
ON UPDATE RESTRICT
|
||||||
ON DELETE RESTRICT
|
ON DELETE RESTRICT
|
||||||
)
|
) WITHOUT ROWID
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для запросов вида: WHERE label=? AND sha256=?
|
-- Индекс для запросов вида: WHERE file=? AND sha256=?
|
||||||
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
|
||||||
ON snapshots(label, sha256)
|
ON snapshots(file, sha256)
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- Индекс для обратного поиска использования blob по sha256
|
-- Индекс для обратного поиска использования blob по sha256
|
||||||
|
|
@ -127,7 +178,7 @@ auto _scheme = [
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE blobs
|
UPDATE blobs
|
||||||
SET refcount = refcount + 1,
|
SET refcount = refcount + 1,
|
||||||
last_seen_utc = CURRENT_TIMESTAMP
|
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int)
|
||||||
WHERE sha256 = NEW.sha256;
|
WHERE sha256 = NEW.sha256;
|
||||||
END
|
END
|
||||||
},
|
},
|
||||||
|
|
@ -162,7 +213,7 @@ auto _scheme = [
|
||||||
|
|
||||||
UPDATE blobs
|
UPDATE blobs
|
||||||
SET refcount = refcount + 1,
|
SET refcount = refcount + 1,
|
||||||
last_seen_utc = CURRENT_TIMESTAMP
|
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int)
|
||||||
WHERE sha256 = NEW.sha256;
|
WHERE sha256 = NEW.sha256;
|
||||||
END
|
END
|
||||||
},
|
},
|
||||||
|
|
@ -235,11 +286,14 @@ 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.label IS NOT OLD.label OR
|
NEW.file IS NOT OLD.file 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
|
||||||
|
|
@ -272,16 +326,16 @@ auto _scheme = [
|
||||||
},
|
},
|
||||||
q{
|
q{
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
-- Удаление записи из labels, если удалён последний snapshot
|
-- Удаление записи из files, если удалён последний snapshot
|
||||||
-- ------------------------------------------------------------
|
-- ------------------------------------------------------------
|
||||||
CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_label
|
CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_file
|
||||||
AFTER DELETE ON snapshots
|
AFTER DELETE ON snapshots
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM labels
|
DELETE FROM files
|
||||||
WHERE id = OLD.label
|
WHERE id = OLD.file
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM snapshots WHERE label = OLD.label
|
SELECT 1 FROM snapshots WHERE file = OLD.file
|
||||||
);
|
);
|
||||||
END;
|
END;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,43 @@
|
||||||
module cdcdb.snapshot;
|
module cdcdb.snapshot;
|
||||||
|
|
||||||
import cdcdb.dblite;
|
import cdcdb.dblite;
|
||||||
|
import cdcdb.lib;
|
||||||
|
|
||||||
import zstd : uncompress;
|
import zstd : uncompress;
|
||||||
|
|
||||||
import std.digest.sha : SHA256, digest;
|
import std.digest.sha : SHA256, digest;
|
||||||
import std.datetime : DateTime;
|
|
||||||
import std.exception : enforce;
|
import std.exception : enforce;
|
||||||
|
import std.datetime : SysTime;
|
||||||
|
|
||||||
/**
|
|
||||||
* Snapshot reader and lifecycle helper.
|
|
||||||
*
|
|
||||||
* This class reconstructs full file content from chunked storage persisted
|
|
||||||
* 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 bytes = s1.data(); // materialize full content in memory
|
|
||||||
*
|
|
||||||
* // or stream into a sink to avoid large allocations:
|
|
||||||
* s1.data((const(ubyte)[] 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
|
||||||
{
|
{
|
||||||
private:
|
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, "Compressed chunk size does not match the expected value");
|
enforce(chunk.zSize == chunk.content.length,
|
||||||
|
"Размер сжатого чанка не совпадает с ожидаемым значением");
|
||||||
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, "Original size does not match the expected value");
|
enforce(chunk.size == bytes.length, "Исходный размер чанка не совпадает с ожидаемым значением");
|
||||||
enforce(chunk.sha256 == digest!SHA256(bytes), "Chunk hash does not match");
|
enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш чанка не совпадает");
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Construct a `Snapshot` from an already fetched `DBSnapshot` row.
|
this(DBLite dblite, DBSnapshot dbSnapshot) { _db = dblite; _snapshot = dbSnapshot; }
|
||||||
///
|
|
||||||
/// Params:
|
|
||||||
/// dblite = database handle
|
|
||||||
/// dbSnapshot = snapshot row (metadata) previously retrieved
|
|
||||||
this(DBLite dblite, DBSnapshot dbSnapshot)
|
|
||||||
{
|
|
||||||
_db = dblite;
|
|
||||||
_snapshot = dbSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a `Snapshot` by loading metadata from the database.
|
|
||||||
///
|
|
||||||
/// Params:
|
|
||||||
/// dblite = database handle
|
|
||||||
/// idSnapshot = snapshot id to load
|
|
||||||
this(DBLite dblite, long idSnapshot)
|
|
||||||
{
|
|
||||||
_db = dblite;
|
|
||||||
_snapshot = _db.getSnapshot(idSnapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Materialize the full file content in memory.
|
|
||||||
///
|
|
||||||
/// Reassembles all chunks in order, validates each chunk SHA-256 and the
|
|
||||||
/// final file SHA-256 (`snapshots.sha256`).
|
|
||||||
///
|
|
||||||
/// Returns: full file content as a newly allocated `ubyte[]`
|
|
||||||
///
|
|
||||||
/// Throws: Exception on any integrity check failure
|
|
||||||
ubyte[] data()
|
ubyte[] data()
|
||||||
{
|
{
|
||||||
auto chunks = _db.getChunks(_snapshot.id);
|
auto chunks = _db.getChunks(_snapshot.id);
|
||||||
|
|
@ -100,20 +53,11 @@ public:
|
||||||
fctx.put(bytes);
|
fctx.put(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(_snapshot.sha256 == fctx.finish(), "File hash does not match");
|
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stream the full file content into a caller-provided sink.
|
|
||||||
///
|
|
||||||
/// This variant avoids allocating a single large buffer. Chunks are
|
|
||||||
/// decoded, verified, and passed to `sink` in order.
|
|
||||||
///
|
|
||||||
/// Params:
|
|
||||||
/// sink = delegate invoked for each verified chunk (may be called many times)
|
|
||||||
///
|
|
||||||
/// 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);
|
||||||
|
|
@ -126,81 +70,49 @@ public:
|
||||||
fctx.put(bytes);
|
fctx.put(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
enforce(_snapshot.sha256 == fctx.finish(), "File hash does not match");
|
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove this snapshot from the database inside a transaction.
|
// -----------------------------
|
||||||
///
|
// Доступ к метаданным снимка
|
||||||
/// Starts an IMMEDIATE transaction, deletes the snapshot row, and commits.
|
// -----------------------------
|
||||||
/// On any failure it rolls back.
|
|
||||||
///
|
|
||||||
/// Returns: `true` if the snapshot row was deleted, `false` otherwise
|
|
||||||
///
|
|
||||||
/// Note: Does not garbage-collect unreferenced blobs; perform that separately.
|
|
||||||
bool remove()
|
|
||||||
{
|
|
||||||
_db.beginImmediate();
|
|
||||||
|
|
||||||
bool ok;
|
/// ID снимка (PRIMARY KEY).
|
||||||
|
@property Identifier id() nothrow @safe { return _snapshot.id; }
|
||||||
scope (exit)
|
/// Имя файла (из таблицы `files`).
|
||||||
{
|
@property string file() const nothrow @safe { return _snapshot.file.path; }
|
||||||
if (!ok)
|
/// Время создания (UTC).
|
||||||
_db.rollback();
|
@property const(SysTime) created() const @safe { return _snapshot.createdUtc.sys; }
|
||||||
}
|
/// Длина исходного файла (байты).
|
||||||
scope (success)
|
@property long length() const nothrow @safe { return _snapshot.sourceLength; }
|
||||||
{
|
/// Ожидаемый SHA-256 всего файла (сырые 32 байта).
|
||||||
_db.commit();
|
@property ubyte[32] sha256() const nothrow @safe { return _snapshot.sha256; }
|
||||||
}
|
/// Статус снимка (строкой).
|
||||||
|
@property string status() const { import std.conv : to; return _snapshot.status.to!string; }
|
||||||
long idDeleted = _db.deleteSnapshot(_snapshot.id);
|
/// Необязательное описание.
|
||||||
|
@property string description() const nothrow @safe { return _snapshot.description; }
|
||||||
ok = true;
|
/// FastCDC: минимальный размер чанка.
|
||||||
|
@property long algoMin() const nothrow @safe { return _snapshot.algoMin; }
|
||||||
return _snapshot.id == idDeleted;
|
/// FastCDC: целевой (нормальный) размер чанка.
|
||||||
}
|
@property long algoNormal() const nothrow @safe { return _snapshot.algoNormal; }
|
||||||
|
/// FastCDC: максимальный размер чанка.
|
||||||
/// Snapshot id (primary key).
|
@property long algoMax() const nothrow @safe { return _snapshot.algoMax; }
|
||||||
@property long id() const nothrow @safe
|
/// FastCDC: строгая маска.
|
||||||
{
|
@property long maskS() const nothrow @safe { return _snapshot.maskS; }
|
||||||
return _snapshot.id;
|
/// FastCDC: слабая маска.
|
||||||
}
|
@property long maskL() const nothrow @safe { return _snapshot.maskL; }
|
||||||
|
/// UID процесса (effective).
|
||||||
/// User-defined label.
|
@property long uid() const nothrow @safe { return _snapshot.uid; }
|
||||||
@property string label() const @safe
|
/// Real UID процесса.
|
||||||
{
|
@property long ruid() const nothrow @safe { return _snapshot.ruid; }
|
||||||
return _snapshot.label;
|
/// Имя пользователя для `uid`.
|
||||||
}
|
@property string uidName() const nothrow @safe { return _snapshot.uidName; }
|
||||||
|
/// Имя пользователя для `ruid`.
|
||||||
/// Creation timestamp (UTC) from the database.
|
@property string ruidName() const nothrow @safe { return _snapshot.ruidName; }
|
||||||
@property DateTime created() const @safe
|
/// Имя процесса (из таблицы `processes`).
|
||||||
{
|
@property string process() const nothrow @safe { return _snapshot.process.name; }
|
||||||
return _snapshot.createdUtc;
|
/// Удобный флаг: снимок «готов».
|
||||||
}
|
@property bool isReady() const nothrow @safe { return _snapshot.status == SnapshotStatus.ready; }
|
||||||
|
/// Удобный флаг: снимок «в процессе».
|
||||||
/// Original file length in bytes.
|
@property bool isPending() const nothrow @safe { return _snapshot.status == SnapshotStatus.pending; }
|
||||||
@property long length() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.sourceLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expected SHA-256 of the full file (32 raw bytes).
|
|
||||||
@property ubyte[32] sha256() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.sha256;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot status as a string (enum to string).
|
|
||||||
@property string status() const
|
|
||||||
{
|
|
||||||
import std.conv : to;
|
|
||||||
|
|
||||||
return _snapshot.status.to!string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional human-readable description.
|
|
||||||
@property string description() const nothrow @safe
|
|
||||||
{
|
|
||||||
return _snapshot.description;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,32 @@
|
||||||
module cdcdb.storage;
|
module cdcdb.storage;
|
||||||
|
|
||||||
import cdcdb.dblite;
|
import cdcdb.dblite;
|
||||||
import cdcdb.core;
|
import cdcdb.storagefile;
|
||||||
import cdcdb.snapshot;
|
import cdcdb.snapshot;
|
||||||
|
import cdcdb.core;
|
||||||
|
import cdcdb.lib : Identifier;
|
||||||
|
|
||||||
import zstd : compress, Level;
|
import zstd : compress, Level;
|
||||||
|
|
||||||
/**
|
import std.exception : enforce;
|
||||||
* High-level storage facade: splits data into CDC chunks, stores chunks/blobs
|
|
||||||
* into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot`
|
struct Context
|
||||||
* objects for retrieval and deletion.
|
{
|
||||||
*
|
long uid; /// UID процесса (effective).
|
||||||
* Features:
|
long ruid; /// Real UID процесса.
|
||||||
* - FastCDC-based content-defined chunking (configurable sizes/masks)
|
string uidName; /// Имя пользователя для UID.
|
||||||
* - Optional Zstandard compression (level configurable)
|
string ruidName; /// Имя пользователя для RUID.
|
||||||
* - Idempotent snapshot creation: skips if identical to the latest for label
|
string process; /// Имя процесса.
|
||||||
*
|
}
|
||||||
* Typical usage:
|
|
||||||
* ---
|
|
||||||
* auto store = new Storage("cdc.sqlite", true, Level.default_);
|
|
||||||
* store.setupCDC(4096, 8192, 16384, 0x3FFF, 0x03FF);
|
|
||||||
*
|
|
||||||
* 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 settings
|
// Настройки CDC
|
||||||
CDC _cdc;
|
CDC _cdc;
|
||||||
size_t _minSize;
|
size_t _minSize;
|
||||||
size_t _normalSize;
|
size_t _normalSize;
|
||||||
|
|
@ -42,6 +34,7 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -50,20 +43,13 @@ private:
|
||||||
_maxSize = maxSize;
|
_maxSize = maxSize;
|
||||||
_maskS = maskS;
|
_maskS = maskS;
|
||||||
_maskL = maskL;
|
_maskL = maskL;
|
||||||
// CDC holds no dynamically allocated state; reinitialization is safe
|
// CDC не хранит динамического состояния, переинициализация безопасна
|
||||||
_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.
|
this(string database, bool zstd = false, int level = Level.base,
|
||||||
///
|
size_t busyTimeout = 3000, size_t maxRetries = 3)
|
||||||
/// Params:
|
|
||||||
/// database = path to SQLite file
|
|
||||||
/// zstd = enable Zstandard compression for stored blobs
|
|
||||||
/// level = Zstd compression level (see `zstd.Level`)
|
|
||||||
/// busyTimeout = SQLite busy timeout in milliseconds
|
|
||||||
/// maxRetries = max retries on SQLITE_BUSY/LOCKED errors
|
|
||||||
this(string database, bool zstd = false, int level = Level.base, size_t busyTimeout = 3000, size_t maxRetries = 3)
|
|
||||||
{
|
{
|
||||||
_db = new DBLite(database, busyTimeout, maxRetries);
|
_db = new DBLite(database, busyTimeout, maxRetries);
|
||||||
_zstd = zstd;
|
_zstd = zstd;
|
||||||
|
|
@ -71,49 +57,29 @@ public:
|
||||||
initCDC();
|
initCDC();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init)
|
||||||
///
|
|
||||||
/// - Splits data with FastCDC using current settings.
|
|
||||||
/// - Optionally compresses chunks with Zstd.
|
|
||||||
/// - Stores unique blobs and links them to the created snapshot.
|
|
||||||
/// - If the latest snapshot for `label` already has the same file SHA-256,
|
|
||||||
/// returns `null` (idempotent).
|
|
||||||
///
|
|
||||||
/// Params:
|
|
||||||
/// label = user-provided snapshot label (file identifier)
|
|
||||||
/// data = raw file bytes
|
|
||||||
/// description = optional human-readable description
|
|
||||||
///
|
|
||||||
/// Returns: a `Snapshot` instance for the created snapshot, or `null`
|
|
||||||
///
|
|
||||||
/// Throws:
|
|
||||||
/// Exception if `data` is empty or on database/storage errors
|
|
||||||
Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init)
|
|
||||||
{
|
{
|
||||||
if (data.length == 0)
|
enforce(data.length > 0, "Данные имеют нулевую длину");
|
||||||
{
|
|
||||||
throw new Exception("Data has zero length");
|
auto dbFile = _db.getFile(file);
|
||||||
}
|
|
||||||
|
|
||||||
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 (dbFile.empty) {
|
||||||
if (_db.isLast(label, sha256))
|
dbFile = _db.addFile(file);
|
||||||
|
} else if (_db.isLast(dbFile.id, sha256)) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
_db.beginImmediate();
|
_db.beginImmediate();
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
|
|
||||||
scope (exit)
|
scope (exit)
|
||||||
|
|
@ -126,11 +92,22 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto dbProcess = _db.getProcess(context.process);
|
||||||
|
|
||||||
|
if (dbProcess.empty) {
|
||||||
|
dbProcess = _db.addProcess(context.process);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метаданные снимка
|
||||||
DBSnapshot dbSnapshot;
|
DBSnapshot dbSnapshot;
|
||||||
|
|
||||||
dbSnapshot.label = label;
|
dbSnapshot.file = dbFile;
|
||||||
dbSnapshot.sha256 = sha256;
|
dbSnapshot.sha256 = sha256;
|
||||||
dbSnapshot.description = description;
|
dbSnapshot.description = description;
|
||||||
dbSnapshot.sourceLength = data.length;
|
dbSnapshot.sourceLength = data.length;
|
||||||
|
|
@ -139,18 +116,19 @@ 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 = dbProcess;
|
||||||
|
|
||||||
auto idSnapshot = _db.addSnapshot(dbSnapshot);
|
enforce(_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;
|
||||||
|
|
@ -158,7 +136,8 @@ 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);
|
||||||
|
|
@ -166,88 +145,107 @@ 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 = dbSnapshot.id;
|
||||||
dbSnapshotChunk.chunkIndex = chunk.index;
|
dbSnapshotChunk.chunkIndex = chunk.index;
|
||||||
dbSnapshotChunk.offset = chunk.offset;
|
dbSnapshotChunk.offset = chunk.offset;
|
||||||
dbSnapshotChunk.sha256 = chunk.sha256;
|
dbSnapshotChunk.sha256 = chunk.sha256;
|
||||||
|
|
||||||
// Link chunk to snapshot
|
enforce(_db.addSnapshotChunk(dbSnapshotChunk), "Не удалось привязать снимок к данным");
|
||||||
_db.addSnapshotChunk(dbSnapshotChunk);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true;
|
ok = true;
|
||||||
|
return new Snapshot(_db, dbSnapshot);
|
||||||
Snapshot snapshot = new Snapshot(_db, idSnapshot);
|
|
||||||
|
|
||||||
return snapshot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete snapshots by label.
|
StorageFile getFile(string path) {
|
||||||
///
|
auto dbFile = _db.getFile(path);
|
||||||
/// Params:
|
if (dbFile.empty) return null;
|
||||||
/// label = snapshot label
|
return new StorageFile(_db, dbFile);
|
||||||
///
|
|
||||||
/// Returns: number of deleted snapshots
|
|
||||||
long removeSnapshots(string label) {
|
|
||||||
return _db.deleteSnapshot(label);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a specific snapshot instance.
|
StorageFile getFile(Identifier id) {
|
||||||
///
|
auto dbFile = _db.getFile(id);
|
||||||
/// Params:
|
if (dbFile.empty) return null;
|
||||||
/// snapshot = `Snapshot` to remove
|
return new StorageFile(_db, dbFile);
|
||||||
///
|
|
||||||
/// Returns: `true` on success, `false` otherwise
|
|
||||||
bool removeSnapshots(Snapshot snapshot) {
|
|
||||||
return removeSnapshots(snapshot.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a snapshot by id.
|
StorageFile[] getFiles() {
|
||||||
///
|
StorageFile[] storageFiles;
|
||||||
/// Params:
|
foreach (dbFile; _db.getFiles()) {
|
||||||
/// idSnapshot = snapshot id
|
storageFiles ~= new StorageFile(_db, dbFile);
|
||||||
///
|
}
|
||||||
/// Returns: `true` if the row was deleted
|
return storageFiles;
|
||||||
bool removeSnapshots(long idSnapshot) {
|
|
||||||
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a `Snapshot` object by id.
|
StorageFile[] findFile(string pattern) {
|
||||||
///
|
StorageFile[] storageFiles;
|
||||||
/// Params:
|
foreach (dbFile; _db.findFile(pattern)) {
|
||||||
/// idSnapshot = snapshot id
|
storageFiles ~= new StorageFile(_db, dbFile);
|
||||||
///
|
}
|
||||||
/// Returns: `Snapshot` handle (metadata loaded lazily via constructor)
|
return storageFiles;
|
||||||
Snapshot getSnapshot(long idSnapshot) {
|
|
||||||
return new Snapshot(_db, idSnapshot);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List snapshots (optionally filtered by label).
|
StorageFile[] findFile(Identifier id) {
|
||||||
///
|
StorageFile[] storageFiles;
|
||||||
/// Params:
|
foreach (dbFile; _db.findFile(id)) {
|
||||||
/// label = filter by exact label; empty string returns all
|
storageFiles ~= new StorageFile(_db, dbFile);
|
||||||
///
|
}
|
||||||
/// Returns: array of `Snapshot` handles
|
return storageFiles;
|
||||||
Snapshot[] getSnapshots(string label = string.init) {
|
}
|
||||||
|
|
||||||
|
Snapshot getSnapshot(Identifier id) {
|
||||||
|
DBSnapshot dbSnapshot = _db.getSnapshot(id);
|
||||||
|
if (dbSnapshot.empty)
|
||||||
|
return null;
|
||||||
|
return new Snapshot(_db, dbSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot[] getSnapshots(Identifier id) {
|
||||||
Snapshot[] snapshots;
|
Snapshot[] snapshots;
|
||||||
|
foreach (dbSnapshot; _db.getSnapshots(id))
|
||||||
foreach (snapshot; _db.getSnapshots(label)) {
|
{
|
||||||
snapshots ~= new Snapshot(_db, snapshot);
|
snapshots ~= new Snapshot(_db, dbSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Library version string.
|
Snapshot[] getSnapshots(string file) {
|
||||||
///
|
Snapshot[] snapshots;
|
||||||
/// Returns: semantic version of the `cdcdb` library
|
foreach (dbSnapshot; _db.getSnapshots(file))
|
||||||
|
{
|
||||||
|
snapshots ~= new Snapshot(_db, dbSnapshot);
|
||||||
|
}
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot[] findSnapshot(Identifier id) {
|
||||||
|
Snapshot[] snapshots;
|
||||||
|
foreach (dbSnapshot; _db.findSnapshot(id)) {
|
||||||
|
snapshots ~= new Snapshot(_db, dbSnapshot);
|
||||||
|
}
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deleteFile(Identifier id) {
|
||||||
|
return _db.deleteFile(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deleteFile(string name) {
|
||||||
|
return _db.deleteFile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deleteSnapshot(Identifier id) {
|
||||||
|
return _db.deleteSnapshot(id);
|
||||||
|
}
|
||||||
|
|
||||||
string getVersion() const @safe nothrow
|
string getVersion() const @safe nothrow
|
||||||
{
|
{
|
||||||
import cdcdb.version_ : cdcdbVersion;
|
import cdcdb.version_ : cdcdbVersion;
|
||||||
|
|
|
||||||
25
source/cdcdb/storagefile.d
Normal file
25
source/cdcdb/storagefile.d
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
module cdcdb.storagefile;
|
||||||
|
|
||||||
|
import cdcdb.snapshot;
|
||||||
|
import cdcdb.lib;
|
||||||
|
import cdcdb.dblite;
|
||||||
|
|
||||||
|
final class StorageFile {
|
||||||
|
private:
|
||||||
|
DBLite _db;
|
||||||
|
DBFile _dbfile;
|
||||||
|
|
||||||
|
public:
|
||||||
|
this(DBLite dblite, DBFile dbfile) { _db = dblite; _dbfile = dbfile; }
|
||||||
|
|
||||||
|
@property ref Identifier id() return { return _dbfile.id; }
|
||||||
|
@property string name() const nothrow @safe { return _dbfile.path; }
|
||||||
|
|
||||||
|
Snapshot[] snapshots() {
|
||||||
|
Snapshot[] snapshots;
|
||||||
|
foreach (dbSnapshot; _db.getSnapshots(_dbfile.id)) {
|
||||||
|
snapshots ~= new Snapshot(_db, dbSnapshot);
|
||||||
|
}
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
module cdcdb.version_;
|
module cdcdb.version_;
|
||||||
|
|
||||||
enum cdcdbVersion = "0.1.1";
|
enum cdcdbVersion = "0.2.0";
|
||||||
|
|
|
||||||
27
test/app.d
27
test/app.d
|
|
@ -3,11 +3,23 @@ import std.stdio : writeln, File;
|
||||||
import std.file : exists, remove, read;
|
import std.file : exists, remove, read;
|
||||||
import zstd : Level;
|
import zstd : Level;
|
||||||
|
|
||||||
|
import core.thread : Thread, msecs, dur;
|
||||||
|
|
||||||
void main()
|
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);
|
||||||
|
|
||||||
|
|
@ -19,17 +31,20 @@ 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, "Версия 1.0");
|
Snapshot snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
|
||||||
if (snap1)
|
if (snap1)
|
||||||
{
|
{
|
||||||
writeln("Создан снимок с ID: ", snap1.id);
|
writeln("Создан снимок с ID: ", snap1.id);
|
||||||
writeln("Метка: ", snap1.label);
|
writeln("Файл: ", snap1.file);
|
||||||
writeln("Размер: ", snap1.length, " байт");
|
writeln("Размер: ", snap1.length, " байт");
|
||||||
writeln("Статус: ", snap1.status);
|
writeln("Статус: ", snap1.status);
|
||||||
|
writeln("Время: ", snap1.created);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Thread.sleep( dur!("msecs")( 50 ) );
|
||||||
|
|
||||||
// Создание второго снимка (обновление)
|
// Создание второго снимка (обновление)
|
||||||
auto snap2 = storage.newSnapshot("example_file", data2, "Версия 2.0");
|
auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
|
||||||
if (snap2)
|
if (snap2)
|
||||||
{
|
{
|
||||||
writeln("Создан обновленный снимок с ID: ", snap2.id);
|
writeln("Создан обновленный снимок с ID: ", snap2.id);
|
||||||
|
|
@ -56,9 +71,9 @@ void main()
|
||||||
writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData));
|
writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаление снимков по метке
|
// Удаление файла
|
||||||
long deleted = storage.removeSnapshots("example_file");
|
if (storage.deleteFile("example_file"))
|
||||||
writeln("Удалено снимков: ", deleted);
|
writeln("Файл example_file удален.");
|
||||||
|
|
||||||
// Проверка: снимки удалены
|
// Проверка: снимки удалены
|
||||||
auto remaining = storage.getSnapshots("example_file");
|
auto remaining = storage.getSnapshots("example_file");
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
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