forked from dlang/cdcdb
Русификация DDoc и сообщений
This commit is contained in:
parent
49ee7a4053
commit
f34b26c2b5
7 changed files with 387 additions and 361 deletions
|
@ -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;
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue