284 lines
7.8 KiB
D
284 lines
7.8 KiB
D
module cdcdb.snapshot;
|
||
|
||
import cdcdb.dblite;
|
||
|
||
import zstd : uncompress;
|
||
|
||
import std.digest.sha : SHA256, digest;
|
||
import std.datetime : DateTime;
|
||
import std.exception : enforce;
|
||
|
||
/**
|
||
* Чтение снимка и управление его жизненным циклом.
|
||
*
|
||
* Класс собирает полный файл из чанков, хранящихся через `DBLite`,
|
||
* проверяет целостность (SHA-256 каждого чанка и итогового файла)
|
||
* и предоставляет безопасное удаление записи о снимке.
|
||
*
|
||
* Пример:
|
||
* ---
|
||
* auto s1 = new Snapshot(db, snapshotId);
|
||
* auto bytes = s1.data(); // материализовать весь контент в память
|
||
*
|
||
* // или потоково, без крупной аллокации:
|
||
* s1.data((const(ubyte)[] part) {
|
||
* // обработать part
|
||
* });
|
||
* ---
|
||
*
|
||
* Заметки:
|
||
* - Все проверки целостности обязательны; любое расхождение вызывает исключение.
|
||
* - Для очень больших файлов предпочтительнее потоковый вариант с делегатом.
|
||
*/
|
||
final class Snapshot
|
||
{
|
||
private:
|
||
DBLite _db;
|
||
DBSnapshot _snapshot;
|
||
|
||
/// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша.
|
||
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
|
||
{
|
||
ubyte[] bytes;
|
||
if (chunk.zstd)
|
||
{
|
||
enforce(chunk.zSize == chunk.content.length,
|
||
"Размер сжатого чанка не совпадает с ожидаемым значением");
|
||
bytes = cast(ubyte[]) uncompress(chunk.content);
|
||
}
|
||
else
|
||
{
|
||
bytes = chunk.content.dup;
|
||
}
|
||
enforce(chunk.size == bytes.length, "Исходный размер чанка не совпадает с ожидаемым значением");
|
||
enforce(chunk.sha256 == digest!SHA256(bytes), "Хеш чанка не совпадает");
|
||
|
||
return bytes;
|
||
}
|
||
|
||
public:
|
||
/// Создать `Snapshot` из уже загруженной строки `DBSnapshot`.
|
||
///
|
||
/// Параметры:
|
||
/// dblite = хэндл базы данных
|
||
/// dbSnapshot = метаданные снимка, полученные ранее
|
||
this(DBLite dblite, DBSnapshot dbSnapshot)
|
||
{
|
||
_db = dblite;
|
||
_snapshot = dbSnapshot;
|
||
}
|
||
|
||
/// Создать `Snapshot`, подгрузив метаданные из базы.
|
||
///
|
||
/// Параметры:
|
||
/// dblite = хэндл базы данных
|
||
/// idSnapshot = идентификатор снимка
|
||
this(DBLite dblite, long idSnapshot)
|
||
{
|
||
_db = dblite;
|
||
_snapshot = _db.getSnapshot(idSnapshot);
|
||
}
|
||
|
||
/// Материализует полный контент файла в память.
|
||
///
|
||
/// Собирает чанки по порядку, проверяет SHA-256 каждого чанка и
|
||
/// итоговый SHA-256 файла (`snapshots.sha256`).
|
||
///
|
||
/// Возвращает: новый буфер `ubyte[]` с полным содержимым.
|
||
///
|
||
/// Бросает: Exception при любой ошибке целостности.
|
||
ubyte[] data()
|
||
{
|
||
auto chunks = _db.getChunks(_snapshot.id);
|
||
ubyte[] content;
|
||
content.reserve(_snapshot.sourceLength);
|
||
|
||
auto fctx = SHA256();
|
||
|
||
foreach (chunk; chunks)
|
||
{
|
||
const(ubyte)[] bytes = getBytes(chunk);
|
||
content ~= bytes;
|
||
fctx.put(bytes);
|
||
}
|
||
|
||
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
||
|
||
return content;
|
||
}
|
||
|
||
/// Потоково передаёт содержимое файла в заданный приёмник.
|
||
///
|
||
/// Избегает одной большой аллокации: чанк декодируется, проверяется
|
||
/// и передаётся в `sink` по порядку.
|
||
///
|
||
/// Параметры:
|
||
/// sink = делегат, вызываемый для каждого проверенного чанка.
|
||
///
|
||
/// Бросает: Exception при любой ошибке целостности.
|
||
void data(void delegate(const(ubyte)[]) sink)
|
||
{
|
||
auto chunks = _db.getChunks(_snapshot.id);
|
||
auto fctx = SHA256();
|
||
|
||
foreach (chunk; chunks)
|
||
{
|
||
const(ubyte)[] bytes = getBytes(chunk);
|
||
sink(bytes);
|
||
fctx.put(bytes);
|
||
}
|
||
|
||
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает");
|
||
}
|
||
|
||
/// Удаляет снимок из базы в транзакции.
|
||
///
|
||
/// Открывает транзакцию IMMEDIATE, удаляет запись о снимке и коммитит.
|
||
/// В случае ошибки откатывает.
|
||
///
|
||
/// Возвращает: `true`, если запись была удалена.
|
||
///
|
||
/// Примечание: не выполняет сборку мусора по блобам.
|
||
bool remove()
|
||
{
|
||
_db.beginImmediate();
|
||
|
||
bool ok;
|
||
|
||
scope (exit)
|
||
{
|
||
if (!ok)
|
||
_db.rollback();
|
||
}
|
||
scope (success)
|
||
{
|
||
_db.commit();
|
||
}
|
||
|
||
long idDeleted = _db.deleteSnapshot(_snapshot.id);
|
||
|
||
ok = true;
|
||
|
||
return _snapshot.id == idDeleted;
|
||
}
|
||
|
||
// -----------------------------
|
||
// Доступ к метаданным снимка
|
||
// -----------------------------
|
||
|
||
/// ID снимка (PRIMARY KEY).
|
||
@property long id() const nothrow @safe
|
||
{
|
||
return _snapshot.id;
|
||
}
|
||
|
||
/// Имя файла (из таблицы `files`).
|
||
@property string file() const nothrow @safe
|
||
{
|
||
return _snapshot.file;
|
||
}
|
||
|
||
/// Время создания (UTC).
|
||
@property DateTime created() const @safe
|
||
{
|
||
return _snapshot.createdUtc;
|
||
}
|
||
|
||
/// Длина исходного файла (байты).
|
||
@property long length() const nothrow @safe
|
||
{
|
||
return _snapshot.sourceLength;
|
||
}
|
||
|
||
/// Ожидаемый SHA-256 всего файла (сырые 32 байта).
|
||
@property ubyte[32] sha256() const nothrow @safe
|
||
{
|
||
return _snapshot.sha256;
|
||
}
|
||
|
||
/// Статус снимка (строкой).
|
||
@property string status() const
|
||
{
|
||
import std.conv : to;
|
||
|
||
return _snapshot.status.to!string;
|
||
}
|
||
|
||
/// Необязательное описание.
|
||
@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;
|
||
}
|
||
}
|