1
0
Fork 0
forked from dlang/cdcdb

Русификация DDoc и сообщений

This commit is contained in:
Alexander Zhirov 2025-09-14 02:30:56 +03:00
parent 49ee7a4053
commit f34b26c2b5
Signed by: alexander
GPG key ID: C8D8BE544A27C511
7 changed files with 387 additions and 361 deletions

View file

@ -1,24 +1,45 @@
/// Модуль базовых структур и алгоритмов CDC (content-defined chunking).
module cdcdb.core;
import std.digest.sha : SHA256, digest;
/// Описание чанка данных.
///
/// Поля:
/// - `index` — порядковый номер чанка, начиная с 1.
/// - `offset` — смещение чанка в исходном буфере (в байтах).
/// - `size` — размер чанка (в байтах).
/// - `sha256` — SHA-256 содержимого (сырые 32 байта).
struct Chunk
{
size_t index; // 1..N
size_t offset; // offset in the source buffer
size_t size; // chunk size
immutable(ubyte)[32] sha256; // hex(SHA-256) of the content
size_t index;
size_t offset;
size_t size;
immutable(ubyte)[32] sha256;
}
// Change Data Capture (CDC)
/// Change Data Capture (CDC) — алгоритм нарезки потока на чанки по содержимому.
///
/// Класс реализует скользящее шифрование (rolling hash) с двумя масками:
/// строгой (`_maskS`) до «нормального» размера и более слабой (`_maskL`) до «максимального».
final class CDC
{
private:
size_t _minSize, _normalSize, _maxSize;
ulong _maskS, _maskL;
// _gear
// Таблица случайных значений Gear (должна быть сгенерирована отдельно в "gear.d")
mixin(import("gear.d"));
/// Вычисляет длину следующего чанка, начиная с начала `src`.
///
/// Параметры:
/// - `src` — оставшийся участок данных.
///
/// Возвращает: длину чанка в байтах.
///
/// Детали:
/// - Если данных меньше либо равно минимальному размеру — возвращает их длину.
/// - Сначала ищется граница по строгой маске до `_normalSize`, затем по слабой до `_maxSize`.
size_t cut(const(ubyte)[] src) pure nothrow @safe @nogc
{
size_t size = src.length;
@ -36,13 +57,13 @@ private:
ulong fingerprint = 0;
size_t index;
// initialization without a cut-check
// Инициализация без проверки на разрез
while (index < _minSize)
{
fingerprint = (fingerprint << 1) + _gear[src[index]];
++index;
}
// strict mask
// Строгая маска
while (index < normalSize)
{
fingerprint = (fingerprint << 1) + _gear[src[index]];
@ -50,7 +71,7 @@ private:
return index;
++index;
}
// weak mask
// Слабая маска
while (index < size)
{
fingerprint = (fingerprint << 1) + _gear[src[index]];
@ -62,10 +83,21 @@ private:
}
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
{
assert(minSize > 0 && minSize < normalSize && normalSize < maxSize,
"Invalid sizes: require min < normal < max and min > 0");
"Некорректные размеры: требуется 0 < min < normal < max");
_minSize = minSize;
_normalSize = normalSize;
_maxSize = maxSize;
@ -73,6 +105,12 @@ public:
_maskL = maskL;
}
/// Разбивает буфер `data` на последовательность чанков.
///
/// Параметры:
/// - `data` — исходные байты.
///
/// Возвращает: массив `Chunk` в порядке следования.
Chunk[] split(const(ubyte)[] data) @safe nothrow
{
Chunk[] chunks;

View file

@ -1,3 +1,5 @@
/// Лёгкая обёртка над SQLite с повторными попытками, схемой БД
/// и удобными структурами данных для снимков/чанков/блобов.
module cdcdb.dblite;
import arsd.sqlite : Sqlite, SqliteResult, DatabaseException;
@ -9,72 +11,85 @@ import std.conv : to;
import std.format : format;
import std.exception : enforce;
/// Статус снимка.
/// - `pending` — создаётся/заполняется;
/// - `ready` — полностью подготовлен.
enum SnapshotStatus : ubyte
{
pending = 0,
ready = 1
}
/// Запись о снимке в БД (агрегированные метаданные).
struct DBSnapshot {
long id;
string file;
ubyte[32] sha256;
string description;
DateTime createdUtc;
long sourceLength;
long algoMin;
long algoNormal;
long algoMax;
long maskS;
long maskL;
SnapshotStatus status;
long uid;
long ruid;
string uidName;
string ruidName;
string process;
long id; /// Идентификатор снимка.
string file; /// Имя файла (таблица `files`).
ubyte[32] sha256; /// Хеш всего файла (SHA-256, 32 байта).
string description; /// Описание/комментарий (может быть пустым).
DateTime createdUtc; /// Время создания (UTC).
long sourceLength; /// Длина исходного файла (байт).
long algoMin; /// FastCDC: минимальный размер чанка.
long algoNormal; /// FastCDC: нормальный (целевой) размер чанка.
long algoMax; /// FastCDC: максимальный размер чанка.
long maskS; /// Строгая маска FastCDC.
long maskL; /// Слабая маска FastCDC.
SnapshotStatus status; /// Статус снимка.
long uid; /// UID процесса (effective).
long ruid; /// Real UID процесса.
string uidName; /// Имя пользователя для `uid`.
string ruidName; /// Имя пользователя для `ruid`.
string process; /// Имя процесса (таблица `processes`).
}
/// Связь снимка с чанками (индексы и хеши).
struct DBSnapshotChunk
{
long snapshotId;
long chunkIndex;
long offset;
ubyte[32] sha256;
long snapshotId; /// ID снимка.
long chunkIndex; /// Порядковый номер чанка в снимке.
long offset; /// Смещение чанка в файле.
ubyte[32] sha256; /// Хеш чанка (SHA-256, 32 байта).
}
/// Запись о блобе (уникальный чанк) в БД.
struct DBBlob
{
ubyte[32] sha256;
ubyte[32] zSha256;
long size;
long zSize;
ubyte[] content;
DateTime createdUtc;
DateTime lastSeenUtc;
long refcount;
bool zstd;
ubyte[32] sha256; /// Хеш исходного содержимого.
ubyte[32] zSha256; /// Хеш сжатого содержимого (если zstd=true).
long size; /// Размер исходного содержимого.
long zSize; /// Размер сжатого содержимого.
ubyte[] content; /// Контент (если хранится в БД).
DateTime createdUtc; /// Время создания (UTC).
DateTime lastSeenUtc; /// Последний доступ (UTC).
long refcount; /// Ссылки на блоб (сколькими снимками используется).
bool zstd; /// Признак, что `content` хранится в сжатом виде.
}
/// Расширенная выборка чанков для восстановления.
/// Содержит и метаданные, и (возможное) содержимое.
struct DBSnapshotChunkData {
long chunkIndex;
long offset;
long size;
ubyte[] content;
bool zstd;
long zSize;
ubyte[32] sha256;
ubyte[32] zSha256;
long chunkIndex; /// Порядковый номер чанка.
long offset; /// Смещение в файле.
long size; /// Размер исходного чанка.
ubyte[] content; /// Содержимое (может быть пустым, если хранится вне БД).
bool zstd; /// Сжат ли контент Zstd.
long zSize; /// Размер сжатого контента.
ubyte[32] sha256; /// Хеш исходного содержимого.
ubyte[32] zSha256; /// Хеш сжатого содержимого.
}
/// Простейший клиент SQLite с:
/// - автоматической инициализацией схемы (при пустой БД);
/// - повторными попытками при блокировках;
/// - удобными методами для CRUD по объектам домена.
final class DBLite : Sqlite
{
private:
string _dbPath;
size_t _maxRetries;
// _scheme
string _dbPath; /// Путь к файлу БД.
size_t _maxRetries; /// Максимум повторов при `busy/locked`.
// SQL-схема (массив строковых запросов).
mixin(import("scheme.d"));
/// Выполняет SQL с повторными попытками при `locked/busy`.
SqliteResult sql(T...)(string queryText, T args)
{
if (_maxRetries == 0) {
@ -92,7 +107,7 @@ private:
if (msg.toLower.canFind("locked", "busy")) {
if (--tryNo == 0) {
throw new Exception(
"Failed to connect to the database after %d failed attempts: %s"
"Не удалось выполнить запрос к базе данных после %d неудачных попыток: %s"
.format(_maxRetries, msg)
);
}
@ -104,7 +119,8 @@ private:
throw new Exception(msg);
}
// Check that the database contains the required tables; otherwise create them
/// Проверяет наличие обязательных таблиц.
/// Если все отсутствуют — создаёт схему; если отсутствует часть — бросает ошибку.
void check()
{
SqliteResult queryResult = sql(
@ -129,7 +145,7 @@ private:
}
enforce(missingTables.length == 0 || missingTables.length == 6,
"Database is corrupted. Missing tables: " ~ missingTables.join(", ")
"База данных повреждена. Отсутствуют таблицы: " ~ missingTables.join(", ")
);
if (missingTables.length == 6)
@ -141,6 +157,8 @@ private:
}
}
/// Переводит текстовую дату из SQLite (`YYYY-MM-DD HH:MM:SS.SSS`)
/// в `DateTime` (ISO 8601 с `T`).
DateTime toDateTime(string sqliteDate)
{
string isoDate = sqliteDate.replace(" ", "T");
@ -148,6 +166,12 @@ private:
}
public:
/// Открывает БД, проверяет/инициализирует схему и настраивает PRAGMA.
///
/// Параметры:
/// - `database` — путь к файлу БД;
/// - `busyTimeout` — таймаут ожидания блокировок (мс);
/// - `maxRetries` — число повторов при `busy/locked`.
this(string database, size_t busyTimeout, size_t maxRetries)
{
_dbPath = database;
@ -163,21 +187,27 @@ public:
query("PRAGMA busy_timeout=%d".format(busyTimeout));
}
/// BEGIN IMMEDIATE.
void beginImmediate()
{
sql("BEGIN IMMEDIATE");
}
/// COMMIT.
void commit()
{
sql("COMMIT");
}
/// ROLLBACK.
void rollback()
{
sql("ROLLBACK");
}
/// Проверяет, совпадает ли последний снимок для `file` с заданным `sha256`.
///
/// Возвращает `true`, если самый свежий снимок этого файла имеет тот же SHA-256.
bool isLast(string file, ubyte[] sha256) {
auto queryResult = sql(
q{
@ -200,6 +230,7 @@ public:
return false;
}
/// Добавляет новый снимок. Возвращает его `id`.
long addSnapshot(DBSnapshot snapshot)
{
auto queryResult = sql(
@ -242,12 +273,13 @@ public:
);
if (queryResult.empty()) {
throw new Exception("Error adding a new snapshot to the database");
throw new Exception("Ошибка добавления нового снимка в базу данных");
}
return queryResult.front()["id"].to!long;
}
/// Добавляет блоб. Возвращает `true`, если вставка произошла (не было конфликта).
bool addBlob(DBBlob blob)
{
auto queryResult = sql(
@ -268,6 +300,7 @@ public:
return !queryResult.empty();
}
/// Добавляет процесс по имени (идемпотентно).
bool addProcess(string name)
{
auto queryResult = sql(
@ -280,6 +313,7 @@ public:
return !queryResult.empty();
}
/// Добавляет файл по имени (идемпотентно).
bool addFile(string name)
{
auto queryResult = sql(
@ -292,6 +326,7 @@ public:
return !queryResult.empty();
}
/// Добавляет пользователя (uid, name) (идемпотентно).
bool addUser(long uid, string name)
{
auto queryResult = sql(
@ -305,6 +340,7 @@ public:
return !queryResult.empty();
}
/// Добавляет связь снимок–чанк.
bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
{
auto queryResult = sql(
@ -322,6 +358,7 @@ public:
return !queryResult.empty();
}
/// Возвращает один снимок по `id`. Если не найден — вернёт пустую структуру.
DBSnapshot getSnapshot(long id)
{
auto queryResult = sql(
@ -381,6 +418,7 @@ public:
return snapshot;
}
/// Возвращает список снимков (опционально фильтр по имени файла).
DBSnapshot[] getSnapshots(string file)
{
auto queryResult = sql(
@ -442,6 +480,7 @@ public:
return snapshots;
}
/// Возвращает последовательность чанков снимка c данными.
DBSnapshotChunkData[] getChunks(long snapshotId)
{
auto queryResult = sql(
@ -476,6 +515,7 @@ public:
return sdchs;
}
/// Удаляет один снимок по `id`. Возвращает `id` удалённой строки или 0.
long deleteSnapshot(long id) {
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id);
@ -485,6 +525,7 @@ public:
return 0;
}
/// Удаляет все снимки по имени файла. Возвращает число удалённых строк.
long deleteSnapshots(string file) {
auto queryResult = sql(
q{

View file

@ -9,26 +9,26 @@ import std.datetime : DateTime;
import std.exception : enforce;
/**
* 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.
* Класс собирает полный файл из чанков, хранящихся через `DBLite`,
* проверяет целостность (SHA-256 каждого чанка и итогового файла)
* и предоставляет безопасное удаление записи о снимке.
*
* Usage:
* Пример:
* ---
* auto s1 = new Snapshot(db, snapshotId);
* auto bytes = s1.data(); // materialize full content in memory
* auto bytes = s1.data(); // материализовать весь контент в память
*
* // or stream into a sink to avoid large allocations:
* // или потоково, без крупной аллокации:
* s1.data((const(ubyte)[] part) {
* // consume part
* // обработать part
* });
* ---
*
* Notes:
* - All integrity checks are enforced; any mismatch throws.
* - `data(void delegate(...))` is preferred for very large files.
* Заметки:
* - Все проверки целостности обязательны; любое расхождение вызывает исключение.
* - Для очень больших файлов предпочтительнее потоковый вариант с делегатом.
*/
final class Snapshot
{
@ -36,55 +36,57 @@ private:
DBLite _db;
DBSnapshot _snapshot;
/// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша.
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
{
ubyte[] bytes;
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);
}
else
{
bytes = chunk.content.dup;
}
enforce(chunk.size == bytes.length, "Original size does not match the expected value");
enforce(chunk.sha256 == digest!SHA256(bytes), "Chunk hash does not match");
enforce(chunk.size == bytes.length, "Исходный размер чанка не совпадает с ожидаемым значением");
enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш чанка не совпадает");
return bytes;
}
public:
/// Construct a `Snapshot` from an already fetched `DBSnapshot` row.
/// Создать `Snapshot` из уже загруженной строки `DBSnapshot`.
///
/// Params:
/// dblite = database handle
/// dbSnapshot = snapshot row (metadata) previously retrieved
/// Параметры:
/// dblite = хэндл базы данных
/// dbSnapshot = метаданные снимка, полученные ранее
this(DBLite dblite, DBSnapshot dbSnapshot)
{
_db = dblite;
_snapshot = dbSnapshot;
}
/// Construct a `Snapshot` by loading metadata from the database.
/// Создать `Snapshot`, подгрузив метаданные из базы.
///
/// Params:
/// dblite = database handle
/// idSnapshot = snapshot id to load
/// Параметры:
/// dblite = хэндл базы данных
/// idSnapshot = идентификатор снимка
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`).
/// Собирает чанки по порядку, проверяет SHA-256 каждого чанка и
/// итоговый SHA-256 файла (`snapshots.sha256`).
///
/// Returns: full file content as a newly allocated `ubyte[]`
/// Возвращает: новый буфер `ubyte[]` с полным содержимым.
///
/// Throws: Exception on any integrity check failure
/// Бросает: Exception при любой ошибке целостности.
ubyte[] data()
{
auto chunks = _db.getChunks(_snapshot.id);
@ -100,20 +102,20 @@ public:
fctx.put(bytes);
}
enforce(_snapshot.sha256 == fctx.finish(), "File hash does not match");
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
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.
/// Избегает одной большой аллокации: чанк декодируется, проверяется
/// и передаётся в `sink` по порядку.
///
/// Params:
/// sink = delegate invoked for each verified chunk (may be called many times)
/// Параметры:
/// sink = делегат, вызываемый для каждого проверенного чанка.
///
/// Throws: Exception on any integrity check failure
/// Бросает: Exception при любой ошибке целостности.
void data(void delegate(const(ubyte)[]) sink)
{
auto chunks = _db.getChunks(_snapshot.id);
@ -126,17 +128,17 @@ public:
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.
/// Открывает транзакцию IMMEDIATE, удаляет запись о снимке и коммитит.
/// В случае ошибки откатывает.
///
/// Returns: `true` if the snapshot row was deleted, `false` otherwise
/// Возвращает: `true`, если запись была удалена.
///
/// Note: Does not garbage-collect unreferenced blobs; perform that separately.
/// Примечание: не выполняет сборку мусора по блобам.
bool remove()
{
_db.beginImmediate();
@ -160,37 +162,41 @@ public:
return _snapshot.id == idDeleted;
}
/// Snapshot id (primary key).
// -----------------------------
// Доступ к метаданным снимка
// -----------------------------
/// ID снимка (PRIMARY KEY).
@property long id() const nothrow @safe
{
return _snapshot.id;
}
/// User-defined label.
@property string file() const @safe
/// Имя файла (из таблицы `files`).
@property string file() const nothrow @safe
{
return _snapshot.file;
}
/// Creation timestamp (UTC) from the database.
/// Время создания (UTC).
@property DateTime created() const @safe
{
return _snapshot.createdUtc;
}
/// Original file length in bytes.
/// Длина исходного файла (байты).
@property long length() const nothrow @safe
{
return _snapshot.sourceLength;
}
/// Expected SHA-256 of the full file (32 raw bytes).
/// Ожидаемый SHA-256 всего файла (сырые 32 байта).
@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;
@ -198,9 +204,81 @@ public:
return _snapshot.status.to!string;
}
/// Optional human-readable description.
/// Необязательное описание.
@property string description() const nothrow @safe
{
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;
}
}

View file

@ -6,43 +6,45 @@ import cdcdb.snapshot;
import zstd : compress, Level;
struct Context {
long uid;
long ruid;
string uidName;
string ruidName;
string process;
/// Контекст создания снимка (идентификаторы и процесс).
struct Context
{
long uid; /// UID процесса (effective).
long ruid; /// Real UID процесса.
string uidName; /// Имя пользователя для UID.
string ruidName; /// Имя пользователя для RUID.
string process; /// Имя процесса.
}
/**
* High-level storage facade: splits data into CDC chunks, stores chunks/blobs
* into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot`
* objects for retrieval and deletion.
* Высокоуровневый фасад для хранения: разбивает данные на чанки CDC,
* сохраняет чанки/блобы в SQLite через `DBLite`, связывает их в снимки
* и возвращает объекты `Snapshot` для последующего чтения и удаления.
*
* Features:
* - FastCDC-based content-defined chunking (configurable sizes/masks)
* - Optional Zstandard compression (level configurable)
* - Idempotent snapshot creation: skips if identical to the latest for label
* Возможности:
* - Разбиение FastCDC (контентно-зависимое, настраиваемые размеры/маски).
* - Опциональное сжатие Zstandard (уровень задаётся).
* - Идемпотентное создание снимков: пропускает, если последний снимок совпадает.
*
* Typical usage:
* Типичное использование:
* ---
* auto store = new Storage("cdc.sqlite", true, Level.default_);
* auto store = new Storage("base.db", true, Level.max);
* store.setupCDC(4096, 8192, 16384, 0x3FFF, 0x03FF);
* Context ctx;
* auto snap = store.newSnapshot("my.txt", data, ctx, "первичный импорт");
* auto bytes = snap.data(); // восстановить содержимое
*
* auto snap = store.newSnapshot("my.txt", data, "initial import");
* auto bytes = snap.data(); // retrieve
*
* auto removed = store.removeSnapshots("my.txt"); // remove by label
* auto removed = store.removeSnapshots("my.txt"); // удалить по имени файла
* ---
*/
final class Storage
{
private:
// Database parameters
// Параметры БД
DBLite _db;
bool _zstd;
int _level;
// CDC settings
// Настройки CDC
CDC _cdc;
size_t _minSize;
size_t _normalSize;
@ -50,6 +52,7 @@ private:
size_t _maskS;
size_t _maskL;
/// Инициализация параметров FastCDC.
void initCDC(size_t minSize = 256, size_t normalSize = 512, size_t maxSize = 1024,
size_t maskS = 0xFF, size_t maskL = 0x0F)
{
@ -58,20 +61,21 @@ private:
_maxSize = maxSize;
_maskS = maskS;
_maskL = maskL;
// CDC holds no dynamically allocated state; reinitialization is safe
// CDC не хранит динамического состояния, переинициализация безопасна
_cdc = new CDC(_minSize, _normalSize, _maxSize, _maskS, _maskL);
}
public:
/// Construct the storage facade and open (or create) the database.
/// Конструктор: открывает/создаёт БД и подготавливает фасад.
///
/// 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)
/// Параметры:
/// database = путь к файлу SQLite
/// zstd = включить Zstd-сжатие для блобов
/// level = уровень сжатия (см. `zstd.Level`)
/// busyTimeout = таймаут ожидания блокировки SQLite (мс)
/// maxRetries = число повторов при SQLITE_BUSY/LOCKED
this(string database, bool zstd = false, int level = Level.base,
size_t busyTimeout = 3000, size_t maxRetries = 3)
{
_db = new DBLite(database, busyTimeout, maxRetries);
_zstd = zstd;
@ -79,49 +83,46 @@ public:
initCDC();
}
/// Reconfigure CDC parameters (takes effect for subsequent snapshots).
///
/// 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)
/// Перенастроить параметры CDC (влияет на будущие снимки).
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize,
size_t maskS, size_t maskL)
{
initCDC(minSize, normalSize, maxSize, maskS, maskL);
}
/// Create a new snapshot from raw data.
/// Создаёт новый снимок из массива байт.
///
/// - 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).
/// - Разбивает данные по текущим параметрам FastCDC.
/// - Опционально сжимает чанки Zstd.
/// - Сохраняет уникальные блобы и связывает их со снимком.
/// - Если последний снимок для файла совпадает по SHA-256, возвращает `null`.
///
/// Params:
/// label = user-provided snapshot label (file identifier)
/// data = raw file bytes
/// description = optional human-readable description
/// Параметры:
/// file = имя файла (метка снимка)
/// data = содержимое файла
/// context = контекст (uid, ruid, процесс и т.д.)
/// description = необязательное описание
///
/// Returns: a `Snapshot` instance for the created snapshot, or `null`
/// Возвращает: объект `Snapshot` или `null`
///
/// Throws:
/// Exception if `data` is empty or on database/storage errors
Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init)
/// Исключения: при пустых данных или ошибках базы
Snapshot newSnapshot(string file, const(ubyte)[] data, Context context,
string description = string.init)
{
if (data.length == 0)
{
throw new Exception("Data has zero length");
throw new Exception("Данные имеют нулевую длину");
}
import std.digest.sha : SHA256, digest;
ubyte[32] sha256 = digest!SHA256(data);
// If the last snapshot for the label matches current content
// Если последний снимок совпадает — пропустить
if (_db.isLast(file, sha256))
return null;
_db.beginImmediate();
bool ok;
scope (exit)
@ -134,16 +135,17 @@ public:
_db.commit();
}
// Запись пользователей/файлов/процессов
_db.addUser(context.uid, context.uidName);
if (context.uid != context.ruid) {
if (context.uid != context.ruid)
{
_db.addUser(context.ruid, context.ruidName);
}
_db.addFile(file);
_db.addProcess(context.process);
// Метаданные снимка
DBSnapshot dbSnapshot;
dbSnapshot.file = file;
dbSnapshot.sha256 = sha256;
dbSnapshot.description = description;
@ -153,22 +155,19 @@ public:
dbSnapshot.algoMax = _maxSize;
dbSnapshot.maskS = _maskS;
dbSnapshot.maskL = _maskL;
dbSnapshot.uid = context.uid;
dbSnapshot.ruid = context.ruid;
dbSnapshot.process = context.process;
auto idSnapshot = _db.addSnapshot(dbSnapshot);
// Чанки и блобы
DBSnapshotChunk dbSnapshotChunk;
DBBlob dbBlob;
dbBlob.zstd = _zstd;
// Split into chunks
Chunk[] chunks = _cdc.split(data);
// Write chunks to DB
foreach (chunk; chunks)
{
dbBlob.sha256 = chunk.sha256;
@ -176,7 +175,8 @@ public:
auto content = data[chunk.offset .. chunk.offset + chunk.size];
if (_zstd) {
if (_zstd)
{
ubyte[] zBytes = compress(content, _level);
size_t zSize = zBytes.length;
ubyte[32] zHash = digest!SHA256(zBytes);
@ -184,11 +184,12 @@ public:
dbBlob.zSize = zSize;
dbBlob.zSha256 = zHash;
dbBlob.content = zBytes;
} else {
}
else
{
dbBlob.content = content.dup;
}
// Store/ensure blob
_db.addBlob(dbBlob);
dbSnapshotChunk.snapshotId = idSnapshot;
@ -196,76 +197,49 @@ public:
dbSnapshotChunk.offset = chunk.offset;
dbSnapshotChunk.sha256 = chunk.sha256;
// Link chunk to snapshot
_db.addSnapshotChunk(dbSnapshotChunk);
}
ok = true;
Snapshot snapshot = new Snapshot(_db, idSnapshot);
return snapshot;
}
/// Delete snapshots by label.
///
/// Params:
/// label = snapshot label
///
/// Returns: number of deleted snapshots
long removeSnapshots(string file) {
return _db.deleteSnapshots(file);
}
/// Delete a specific snapshot instance.
///
/// Params:
/// snapshot = `Snapshot` to remove
///
/// Returns: `true` on success, `false` otherwise
bool removeSnapshot(Snapshot snapshot) {
return removeSnapshot(snapshot.id);
}
/// Delete a snapshot by id.
///
/// Params:
/// idSnapshot = snapshot id
///
/// Returns: `true` if the row was deleted
bool removeSnapshot(long idSnapshot) {
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
}
/// Get a `Snapshot` object by id.
///
/// Params:
/// idSnapshot = snapshot id
///
/// Returns: `Snapshot` handle (metadata loaded lazily via constructor)
Snapshot getSnapshot(long idSnapshot) {
return new Snapshot(_db, idSnapshot);
}
/// List snapshots (optionally filtered by label).
///
/// Params:
/// label = filter by exact label; empty string returns all
///
/// Returns: array of `Snapshot` handles
Snapshot[] getSnapshots(string file = string.init) {
/// Удаляет все снимки по имени файла.
long removeSnapshots(string file)
{
return _db.deleteSnapshots(file);
}
/// Удаляет конкретный снимок по объекту `Snapshot`.
bool removeSnapshot(Snapshot snapshot)
{
return removeSnapshot(snapshot.id);
}
/// Удаляет снимок по id.
bool removeSnapshot(long idSnapshot)
{
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
}
/// Возвращает `Snapshot` по id.
Snapshot getSnapshot(long idSnapshot)
{
return new Snapshot(_db, idSnapshot);
}
/// Возвращает список снимков (опционально фильтр по имени файла).
Snapshot[] getSnapshots(string file = string.init)
{
Snapshot[] snapshots;
foreach (snapshot; _db.getSnapshots(file)) {
foreach (snapshot; _db.getSnapshots(file))
{
snapshots ~= new Snapshot(_db, snapshot);
}
return snapshots;
}
/// Library version string.
///
/// Returns: semantic version of the `cdcdb` library
/// Версия библиотеки.
string getVersion() const @safe nothrow
{
import cdcdb.version_ : cdcdbVersion;