forked from dlang/cdcdb
Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
0fc56e7c04 | |||
1f50b21457 |
14 changed files with 1015 additions and 596 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -15,4 +15,4 @@ cdcdb-test-*
|
|||
*.obj
|
||||
*.lst
|
||||
bin
|
||||
lib
|
||||
/lib
|
||||
|
|
2
dub.json
2
dub.json
|
@ -7,7 +7,7 @@
|
|||
"license": "BSL-1.0",
|
||||
"name": "cdcdb",
|
||||
"dependencies": {
|
||||
"arsd-official:sqlite": "~>12.0.0",
|
||||
"d2sqlite3": "~>1.0.0",
|
||||
"zstd": "~>0.2.1"
|
||||
},
|
||||
"stringImportPaths": [
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"fileVersion": 1,
|
||||
"versions": {
|
||||
"arsd-official": "12.0.0",
|
||||
"d2sqlite3": "1.0.0",
|
||||
"zstd": "0.2.1"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
110
source/cdcdb/lib/hash.d
Normal file
110
source/cdcdb/lib/hash.d
Normal file
|
@ -0,0 +1,110 @@
|
|||
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(immutable(ubyte[]) data)
|
||||
{
|
||||
assert(data.length <= 16);
|
||||
_data = data.dup;
|
||||
}
|
||||
|
||||
this(ref const ubyte[16] data)
|
||||
{
|
||||
assert(data.length <= 16);
|
||||
_data = data.dup;
|
||||
}
|
||||
|
||||
void opAssign(immutable(ubyte[]) 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;
|
||||
|
||||
public import cdcdb.lib;
|
||||
public import cdcdb.storage;
|
||||
public import cdcdb.storagefile;
|
||||
public import cdcdb.snapshot;
|
||||
|
|
|
@ -21,10 +21,10 @@ auto _scheme = [
|
|||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS processes (
|
||||
-- идентификатор процесса
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
id BLOB PRIMARY KEY,
|
||||
-- имя процесса
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)
|
||||
) WITHOUT ROWID
|
||||
},
|
||||
q{
|
||||
-- Индекс по имени процесса
|
||||
|
@ -37,10 +37,10 @@ auto _scheme = [
|
|||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
-- идентификатор файла
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
id BLOB PRIMARY KEY,
|
||||
-- имя файла
|
||||
name TEXT NOT NULL UNIQUE
|
||||
)
|
||||
) WITHOUT ROWID
|
||||
},
|
||||
q{
|
||||
-- Индекс по имени файла
|
||||
|
@ -53,15 +53,15 @@ auto _scheme = [
|
|||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS snapshots (
|
||||
-- идентификатор снимка
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
id BLOB PRIMARY KEY,
|
||||
-- Файл
|
||||
file INTEGER NOT NULL,
|
||||
file BLOB NOT NULL,
|
||||
-- SHA-256 всего файла (BLOB(32))
|
||||
sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
|
||||
-- Комментарий/описание
|
||||
description TEXT DEFAULT NULL,
|
||||
-- время создания (UTC)
|
||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
created_utc INTEGER NOT NULL,
|
||||
-- длина исходного файла в байтах
|
||||
source_length INTEGER NOT NULL,
|
||||
-- UID пользователя (эффективный)
|
||||
|
@ -69,7 +69,7 @@ auto _scheme = [
|
|||
-- RUID пользователя (реальный)
|
||||
ruid INTEGER NOT NULL,
|
||||
-- Процесс
|
||||
process INTEGER NOT NULL,
|
||||
process BLOB NOT NULL,
|
||||
-- FastCDC: минимальный размер чанка
|
||||
algo_min INTEGER NOT NULL,
|
||||
-- FastCDC: целевой размер чанка
|
||||
|
@ -100,7 +100,7 @@ auto _scheme = [
|
|||
REFERENCES files(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
)
|
||||
) WITHOUT ROWID
|
||||
},
|
||||
q{
|
||||
-- ------------------------------------------------------------
|
||||
|
@ -118,9 +118,9 @@ auto _scheme = [
|
|||
-- байты (сжатые при zstd=1, иначе исходные)
|
||||
content BLOB NOT NULL,
|
||||
-- время создания записи (UTC)
|
||||
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
created_utc INTEGER NOT NULL,
|
||||
-- время последней ссылки (UTC)
|
||||
last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
last_seen_utc INTEGER NOT NULL,
|
||||
-- число ссылок из snapshot_chunks
|
||||
refcount INTEGER NOT NULL DEFAULT 0,
|
||||
-- 0=нет сжатия, 1=zstd
|
||||
|
@ -132,7 +132,7 @@ auto _scheme = [
|
|||
(zstd = 0 AND length(content) = size)
|
||||
),
|
||||
CHECK (z_sha256 IS NULL OR length(z_sha256) = 32)
|
||||
)
|
||||
) WITHOUT ROWID
|
||||
},
|
||||
q{
|
||||
-- ------------------------------------------------------------
|
||||
|
@ -140,7 +140,7 @@ auto _scheme = [
|
|||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS snapshot_chunks (
|
||||
-- FK -> snapshots.id
|
||||
snapshot_id INTEGER NOT NULL,
|
||||
snapshot_id BLOB NOT NULL,
|
||||
-- порядковый номер чанка в снимке
|
||||
chunk_index INTEGER NOT NULL,
|
||||
-- смещение чанка в исходном файле, байт
|
||||
|
@ -156,7 +156,7 @@ auto _scheme = [
|
|||
REFERENCES blobs(sha256)
|
||||
ON UPDATE RESTRICT
|
||||
ON DELETE RESTRICT
|
||||
)
|
||||
) WITHOUT ROWID
|
||||
},
|
||||
q{
|
||||
-- Индекс для запросов вида: WHERE file=? AND sha256=?
|
||||
|
@ -178,7 +178,7 @@ auto _scheme = [
|
|||
BEGIN
|
||||
UPDATE blobs
|
||||
SET refcount = refcount + 1,
|
||||
last_seen_utc = CURRENT_TIMESTAMP
|
||||
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int)
|
||||
WHERE sha256 = NEW.sha256;
|
||||
END
|
||||
},
|
||||
|
@ -213,7 +213,7 @@ auto _scheme = [
|
|||
|
||||
UPDATE blobs
|
||||
SET refcount = refcount + 1,
|
||||
last_seen_utc = CURRENT_TIMESTAMP
|
||||
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int)
|
||||
WHERE sha256 = NEW.sha256;
|
||||
END
|
||||
},
|
||||
|
|
|
@ -1,42 +1,21 @@
|
|||
module cdcdb.snapshot;
|
||||
|
||||
import cdcdb.dblite;
|
||||
import cdcdb.lib;
|
||||
|
||||
import zstd : uncompress;
|
||||
|
||||
import std.digest.sha : SHA256, digest;
|
||||
import std.datetime : DateTime;
|
||||
import std.exception : enforce;
|
||||
import std.datetime : SysTime;
|
||||
|
||||
/**
|
||||
* Чтение снимка и управление его жизненным циклом.
|
||||
*
|
||||
* Класс собирает полный файл из чанков, хранящихся через `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;
|
||||
|
@ -57,36 +36,8 @@ private:
|
|||
}
|
||||
|
||||
public:
|
||||
/// Создать `Snapshot` из уже загруженной строки `DBSnapshot`.
|
||||
///
|
||||
/// Параметры:
|
||||
/// dblite = хэндл базы данных
|
||||
/// dbSnapshot = метаданные снимка, полученные ранее
|
||||
this(DBLite dblite, DBSnapshot dbSnapshot)
|
||||
{
|
||||
_db = dblite;
|
||||
_snapshot = 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);
|
||||
|
@ -107,15 +58,6 @@ public:
|
|||
return content;
|
||||
}
|
||||
|
||||
/// Потоково передаёт содержимое файла в заданный приёмник.
|
||||
///
|
||||
/// Избегает одной большой аллокации: чанк декодируется, проверяется
|
||||
/// и передаётся в `sink` по порядку.
|
||||
///
|
||||
/// Параметры:
|
||||
/// sink = делегат, вызываемый для каждого проверенного чанка.
|
||||
///
|
||||
/// Бросает: Exception при любой ошибке целостности.
|
||||
void data(void delegate(const(ubyte)[]) sink)
|
||||
{
|
||||
auto chunks = _db.getChunks(_snapshot.id);
|
||||
|
@ -131,154 +73,46 @@ public:
|
|||
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;
|
||||
}
|
||||
|
||||
@property Identifier id() nothrow @safe { return _snapshot.id; }
|
||||
/// Имя файла (из таблицы `files`).
|
||||
@property string file() const nothrow @safe
|
||||
{
|
||||
return _snapshot.file;
|
||||
}
|
||||
|
||||
@property string file() const nothrow @safe { return _snapshot.file.path; }
|
||||
/// Время создания (UTC).
|
||||
@property DateTime created() const @safe
|
||||
{
|
||||
return _snapshot.createdUtc;
|
||||
}
|
||||
|
||||
@property const(SysTime) created() const @safe { return _snapshot.createdUtc.sys; }
|
||||
/// Длина исходного файла (байты).
|
||||
@property long length() const nothrow @safe
|
||||
{
|
||||
return _snapshot.sourceLength;
|
||||
}
|
||||
|
||||
@property long length() const nothrow @safe { return _snapshot.sourceLength; }
|
||||
/// Ожидаемый SHA-256 всего файла (сырые 32 байта).
|
||||
@property ubyte[32] sha256() const nothrow @safe
|
||||
{
|
||||
return _snapshot.sha256;
|
||||
}
|
||||
|
||||
@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 status() const { import std.conv : to; return _snapshot.status.to!string; }
|
||||
/// Необязательное описание.
|
||||
@property string description() const nothrow @safe
|
||||
{
|
||||
return _snapshot.description;
|
||||
}
|
||||
|
||||
@property string description() const nothrow @safe { return _snapshot.description; }
|
||||
/// FastCDC: минимальный размер чанка.
|
||||
@property long algoMin() const nothrow @safe
|
||||
{
|
||||
return _snapshot.algoMin;
|
||||
}
|
||||
|
||||
@property long algoMin() const nothrow @safe { return _snapshot.algoMin; }
|
||||
/// FastCDC: целевой (нормальный) размер чанка.
|
||||
@property long algoNormal() const nothrow @safe
|
||||
{
|
||||
return _snapshot.algoNormal;
|
||||
}
|
||||
|
||||
@property long algoNormal() const nothrow @safe { return _snapshot.algoNormal; }
|
||||
/// FastCDC: максимальный размер чанка.
|
||||
@property long algoMax() const nothrow @safe
|
||||
{
|
||||
return _snapshot.algoMax;
|
||||
}
|
||||
|
||||
@property long algoMax() const nothrow @safe { return _snapshot.algoMax; }
|
||||
/// FastCDC: строгая маска.
|
||||
@property long maskS() const nothrow @safe
|
||||
{
|
||||
return _snapshot.maskS;
|
||||
}
|
||||
|
||||
@property long maskS() const nothrow @safe { return _snapshot.maskS; }
|
||||
/// FastCDC: слабая маска.
|
||||
@property long maskL() const nothrow @safe
|
||||
{
|
||||
return _snapshot.maskL;
|
||||
}
|
||||
|
||||
@property long maskL() const nothrow @safe { return _snapshot.maskL; }
|
||||
/// UID процесса (effective).
|
||||
@property long uid() const nothrow @safe
|
||||
{
|
||||
return _snapshot.uid;
|
||||
}
|
||||
|
||||
@property long uid() const nothrow @safe { return _snapshot.uid; }
|
||||
/// Real UID процесса.
|
||||
@property long ruid() const nothrow @safe
|
||||
{
|
||||
return _snapshot.ruid;
|
||||
}
|
||||
|
||||
@property long ruid() const nothrow @safe { return _snapshot.ruid; }
|
||||
/// Имя пользователя для `uid`.
|
||||
@property string uidName() const nothrow @safe
|
||||
{
|
||||
return _snapshot.uidName;
|
||||
}
|
||||
|
||||
@property string uidName() const nothrow @safe { return _snapshot.uidName; }
|
||||
/// Имя пользователя для `ruid`.
|
||||
@property string ruidName() const nothrow @safe
|
||||
{
|
||||
return _snapshot.ruidName;
|
||||
}
|
||||
|
||||
@property string ruidName() const nothrow @safe { return _snapshot.ruidName; }
|
||||
/// Имя процесса (из таблицы `processes`).
|
||||
@property string process() const nothrow @safe
|
||||
{
|
||||
return _snapshot.process;
|
||||
}
|
||||
|
||||
@property string process() const nothrow @safe { return _snapshot.process.name; }
|
||||
/// Удобный флаг: снимок «готов».
|
||||
@property bool isReady() const nothrow @safe
|
||||
{
|
||||
return _snapshot.status == SnapshotStatus.ready;
|
||||
}
|
||||
|
||||
@property bool isReady() const nothrow @safe { return _snapshot.status == SnapshotStatus.ready; }
|
||||
/// Удобный флаг: снимок «в процессе».
|
||||
@property bool isPending() const nothrow @safe
|
||||
{
|
||||
return _snapshot.status == SnapshotStatus.pending;
|
||||
}
|
||||
@property bool isPending() const nothrow @safe { return _snapshot.status == SnapshotStatus.pending; }
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
module cdcdb.storage;
|
||||
|
||||
import cdcdb.dblite;
|
||||
import cdcdb.core;
|
||||
import cdcdb.storagefile;
|
||||
import cdcdb.snapshot;
|
||||
import cdcdb.core;
|
||||
import cdcdb.lib : Identifier;
|
||||
|
||||
import zstd : compress, Level;
|
||||
|
||||
/// Контекст создания снимка (идентификаторы и процесс).
|
||||
import std.exception : enforce;
|
||||
|
||||
struct Context
|
||||
{
|
||||
long uid; /// UID процесса (effective).
|
||||
|
@ -16,27 +19,6 @@ struct Context
|
|||
string process; /// Имя процесса.
|
||||
}
|
||||
|
||||
/**
|
||||
* Высокоуровневый фасад для хранения: разбивает данные на чанки CDC,
|
||||
* сохраняет чанки/блобы в SQLite через `DBLite`, связывает их в снимки
|
||||
* и возвращает объекты `Snapshot` для последующего чтения и удаления.
|
||||
*
|
||||
* Возможности:
|
||||
* - Разбиение FastCDC (контентно-зависимое, настраиваемые размеры/маски).
|
||||
* - Опциональное сжатие Zstandard (уровень задаётся).
|
||||
* - Идемпотентное создание снимков: пропускает, если последний снимок совпадает.
|
||||
*
|
||||
* Типичное использование:
|
||||
* ---
|
||||
* 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 removed = store.removeSnapshots("my.txt"); // удалить по имени файла
|
||||
* ---
|
||||
*/
|
||||
final class Storage
|
||||
{
|
||||
private:
|
||||
|
@ -66,14 +48,6 @@ private:
|
|||
}
|
||||
|
||||
public:
|
||||
/// Конструктор: открывает/создаёт БД и подготавливает фасад.
|
||||
///
|
||||
/// Параметры:
|
||||
/// 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)
|
||||
{
|
||||
|
@ -83,44 +57,27 @@ public:
|
|||
initCDC();
|
||||
}
|
||||
|
||||
/// Перенастроить параметры CDC (влияет на будущие снимки).
|
||||
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize,
|
||||
size_t maskS, size_t maskL)
|
||||
{
|
||||
initCDC(minSize, normalSize, maxSize, maskS, maskL);
|
||||
}
|
||||
|
||||
/// Создаёт новый снимок из массива байт.
|
||||
///
|
||||
/// - Разбивает данные по текущим параметрам FastCDC.
|
||||
/// - Опционально сжимает чанки Zstd.
|
||||
/// - Сохраняет уникальные блобы и связывает их со снимком.
|
||||
/// - Если последний снимок для файла совпадает по SHA-256, возвращает `null`.
|
||||
///
|
||||
/// Параметры:
|
||||
/// file = имя файла (метка снимка)
|
||||
/// data = содержимое файла
|
||||
/// context = контекст (uid, ruid, процесс и т.д.)
|
||||
/// description = необязательное описание
|
||||
///
|
||||
/// Возвращает: объект `Snapshot` или `null`
|
||||
///
|
||||
/// Исключения: при пустых данных или ошибках базы
|
||||
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("Данные имеют нулевую длину");
|
||||
}
|
||||
enforce(data.length > 0, "Данные имеют нулевую длину");
|
||||
|
||||
auto dbFile = _db.getFile(file);
|
||||
|
||||
import std.digest.sha : SHA256, digest;
|
||||
|
||||
ubyte[32] sha256 = digest!SHA256(data);
|
||||
|
||||
// Если последний снимок совпадает — пропустить
|
||||
if (_db.isLast(file, sha256))
|
||||
if (dbFile.empty) {
|
||||
dbFile = _db.addFile(file);
|
||||
} else if (_db.isLast(dbFile.id, sha256)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_db.beginImmediate();
|
||||
bool ok;
|
||||
|
@ -135,18 +92,22 @@ public:
|
|||
_db.commit();
|
||||
}
|
||||
|
||||
// Запись пользователей/файлов/процессов
|
||||
_db.addUser(context.uid, context.uidName);
|
||||
if (context.uid != context.ruid)
|
||||
{
|
||||
_db.addUser(context.ruid, context.ruidName);
|
||||
}
|
||||
_db.addFile(file);
|
||||
_db.addProcess(context.process);
|
||||
|
||||
auto dbProcess = _db.getProcess(context.process);
|
||||
|
||||
if (dbProcess.empty) {
|
||||
dbProcess = _db.addProcess(context.process);
|
||||
}
|
||||
|
||||
// Метаданные снимка
|
||||
DBSnapshot dbSnapshot;
|
||||
dbSnapshot.file = file;
|
||||
|
||||
dbSnapshot.file = dbFile;
|
||||
dbSnapshot.sha256 = sha256;
|
||||
dbSnapshot.description = description;
|
||||
dbSnapshot.sourceLength = data.length;
|
||||
|
@ -157,9 +118,9 @@ public:
|
|||
dbSnapshot.maskL = _maskL;
|
||||
dbSnapshot.uid = context.uid;
|
||||
dbSnapshot.ruid = context.ruid;
|
||||
dbSnapshot.process = context.process;
|
||||
dbSnapshot.process = dbProcess;
|
||||
|
||||
auto idSnapshot = _db.addSnapshot(dbSnapshot);
|
||||
enforce(_db.addSnapshot(dbSnapshot), "Не удалось добавить новый снимок в базу данных");
|
||||
|
||||
// Чанки и блобы
|
||||
DBSnapshotChunk dbSnapshotChunk;
|
||||
|
@ -192,54 +153,99 @@ public:
|
|||
|
||||
_db.addBlob(dbBlob);
|
||||
|
||||
dbSnapshotChunk.snapshotId = idSnapshot;
|
||||
dbSnapshotChunk.snapshotId = dbSnapshot.id;
|
||||
dbSnapshotChunk.chunkIndex = chunk.index;
|
||||
dbSnapshotChunk.offset = chunk.offset;
|
||||
dbSnapshotChunk.sha256 = chunk.sha256;
|
||||
|
||||
_db.addSnapshotChunk(dbSnapshotChunk);
|
||||
enforce(_db.addSnapshotChunk(dbSnapshotChunk), "Не удалось привязать снимок к данным");
|
||||
}
|
||||
|
||||
ok = true;
|
||||
return new Snapshot(_db, idSnapshot);
|
||||
return new Snapshot(_db, dbSnapshot);
|
||||
}
|
||||
|
||||
/// Удаляет все снимки по имени файла.
|
||||
long removeSnapshots(string file)
|
||||
{
|
||||
return _db.deleteSnapshots(file);
|
||||
StorageFile getFile(string path) {
|
||||
auto dbFile = _db.getFile(path);
|
||||
if (dbFile.empty) return null;
|
||||
return new StorageFile(_db, dbFile);
|
||||
}
|
||||
|
||||
/// Удаляет конкретный снимок по объекту `Snapshot`.
|
||||
bool removeSnapshot(Snapshot snapshot)
|
||||
{
|
||||
return removeSnapshot(snapshot.id);
|
||||
StorageFile getFile(Identifier id) {
|
||||
auto dbFile = _db.getFile(id);
|
||||
if (dbFile.empty) return null;
|
||||
return new StorageFile(_db, dbFile);
|
||||
}
|
||||
|
||||
/// Удаляет снимок по id.
|
||||
bool removeSnapshot(long idSnapshot)
|
||||
{
|
||||
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
|
||||
StorageFile[] getFiles() {
|
||||
StorageFile[] storageFiles;
|
||||
foreach (dbFile; _db.getFiles()) {
|
||||
storageFiles ~= new StorageFile(_db, dbFile);
|
||||
}
|
||||
return storageFiles;
|
||||
}
|
||||
|
||||
/// Возвращает `Snapshot` по id.
|
||||
Snapshot getSnapshot(long idSnapshot)
|
||||
{
|
||||
return new Snapshot(_db, idSnapshot);
|
||||
StorageFile[] findFile(string pattern) {
|
||||
StorageFile[] storageFiles;
|
||||
foreach (dbFile; _db.findFile(pattern)) {
|
||||
storageFiles ~= new StorageFile(_db, dbFile);
|
||||
}
|
||||
return storageFiles;
|
||||
}
|
||||
|
||||
/// Возвращает список снимков (опционально фильтр по имени файла).
|
||||
Snapshot[] getSnapshots(string file = string.init)
|
||||
{
|
||||
StorageFile[] findFile(Identifier id) {
|
||||
StorageFile[] storageFiles;
|
||||
foreach (dbFile; _db.findFile(id)) {
|
||||
storageFiles ~= new StorageFile(_db, dbFile);
|
||||
}
|
||||
return storageFiles;
|
||||
}
|
||||
|
||||
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;
|
||||
foreach (snapshot; _db.getSnapshots(file))
|
||||
foreach (dbSnapshot; _db.getSnapshots(id))
|
||||
{
|
||||
snapshots ~= new Snapshot(_db, snapshot);
|
||||
snapshots ~= new Snapshot(_db, dbSnapshot);
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
/// Версия библиотеки.
|
||||
Snapshot[] getSnapshots(string file) {
|
||||
Snapshot[] snapshots;
|
||||
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
|
||||
{
|
||||
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_;
|
||||
|
||||
enum cdcdbVersion = "0.1.0";
|
||||
enum cdcdbVersion = "0.2.0";
|
||||
|
|
27
test/app.d
27
test/app.d
|
@ -3,14 +3,14 @@ import std.stdio : writeln, File;
|
|||
import std.file : exists, remove, read;
|
||||
import zstd : Level;
|
||||
|
||||
import core.thread : Thread, msecs, dur;
|
||||
|
||||
void main()
|
||||
{
|
||||
// Создаем временную базу для примера
|
||||
string dbPath = "./bin/example.db";
|
||||
|
||||
if (exists(dbPath)) {
|
||||
remove(dbPath);
|
||||
}
|
||||
if (exists(dbPath)) { remove(dbPath); }
|
||||
|
||||
Context context;
|
||||
|
||||
|
@ -31,15 +31,18 @@ void main()
|
|||
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
|
||||
|
||||
// Создание первого снимка
|
||||
auto snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
|
||||
Snapshot snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
|
||||
if (snap1)
|
||||
{
|
||||
writeln("Создан снимок с ID: ", snap1.id);
|
||||
writeln("Файл: ", snap1.file);
|
||||
writeln("Размер: ", snap1.length, " байт");
|
||||
writeln("Статус: ", snap1.status);
|
||||
writeln("Время: ", snap1.created);
|
||||
}
|
||||
|
||||
Thread.sleep( dur!("msecs")( 50 ) );
|
||||
|
||||
// Создание второго снимка (обновление)
|
||||
auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
|
||||
if (snap2)
|
||||
|
@ -68,14 +71,14 @@ void main()
|
|||
writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData));
|
||||
}
|
||||
|
||||
// Удаление снимков по метке
|
||||
long deleted = storage.removeSnapshots("example_file");
|
||||
writeln("Удалено снимков: ", deleted);
|
||||
// Удаление файла
|
||||
// if (storage.deleteFile("example_file"))
|
||||
// writeln("Файл example_file удален.");
|
||||
|
||||
// Проверка: снимки удалены
|
||||
auto remaining = storage.getSnapshots("example_file");
|
||||
assert(remaining.length == 0);
|
||||
writeln("Все снимки удалены.");
|
||||
// // Проверка: снимки удалены
|
||||
// auto remaining = storage.getSnapshots("example_file");
|
||||
// assert(remaining.length == 0);
|
||||
// writeln("Все снимки удалены.");
|
||||
|
||||
writeln("Версия библиотеки: ", storage.getVersion());
|
||||
// writeln("Версия библиотеки: ", storage.getVersion());
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue