1
0
Fork 0
forked from dlang/cdcdb

Compare commits

..

No commits in common. "d2sqlite3" and "master" have entirely different histories.

14 changed files with 605 additions and 1024 deletions

2
.gitignore vendored
View file

@ -15,4 +15,4 @@ cdcdb-test-*
*.obj *.obj
*.lst *.lst
bin bin
/lib lib

View file

@ -7,7 +7,7 @@
"license": "BSL-1.0", "license": "BSL-1.0",
"name": "cdcdb", "name": "cdcdb",
"dependencies": { "dependencies": {
"d2sqlite3": "~>1.0.0", "arsd-official:sqlite": "~>12.0.0",
"zstd": "~>0.2.1" "zstd": "~>0.2.1"
}, },
"stringImportPaths": [ "stringImportPaths": [

View file

@ -2,7 +2,6 @@
"fileVersion": 1, "fileVersion": 1,
"versions": { "versions": {
"arsd-official": "12.0.0", "arsd-official": "12.0.0",
"d2sqlite3": "1.0.0",
"zstd": "0.2.1" "zstd": "0.2.1"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -1,110 +0,0 @@
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;
}
}

View file

@ -1,4 +0,0 @@
module cdcdb.lib;
public import cdcdb.lib.hash;
public import cdcdb.lib.uts;

View file

@ -1,61 +0,0 @@
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;
}
}

View file

@ -1,6 +1,4 @@
module cdcdb; module cdcdb;
public import cdcdb.lib;
public import cdcdb.storage; public import cdcdb.storage;
public import cdcdb.storagefile;
public import cdcdb.snapshot; public import cdcdb.snapshot;

View file

@ -21,10 +21,10 @@ auto _scheme = [
-- ------------------------------------------------------------ -- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS processes ( CREATE TABLE IF NOT EXISTS processes (
-- идентификатор процесса -- идентификатор процесса
id BLOB PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
-- имя процесса -- имя процесса
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
) WITHOUT ROWID )
}, },
q{ q{
-- Индекс по имени процесса -- Индекс по имени процесса
@ -37,10 +37,10 @@ auto _scheme = [
-- ------------------------------------------------------------ -- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS files ( CREATE TABLE IF NOT EXISTS files (
-- идентификатор файла -- идентификатор файла
id BLOB PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
-- имя файла -- имя файла
name TEXT NOT NULL UNIQUE name TEXT NOT NULL UNIQUE
) WITHOUT ROWID )
}, },
q{ q{
-- Индекс по имени файла -- Индекс по имени файла
@ -53,15 +53,15 @@ auto _scheme = [
-- ------------------------------------------------------------ -- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS snapshots ( CREATE TABLE IF NOT EXISTS snapshots (
-- идентификатор снимка -- идентификатор снимка
id BLOB PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Файл -- Файл
file BLOB NOT NULL, file INTEGER NOT NULL,
-- SHA-256 всего файла (BLOB(32)) -- SHA-256 всего файла (BLOB(32))
sha256 BLOB NOT NULL CHECK (length(sha256) = 32), sha256 BLOB NOT NULL CHECK (length(sha256) = 32),
-- Комментарий/описание -- Комментарий/описание
description TEXT DEFAULT NULL, description TEXT DEFAULT NULL,
-- время создания (UTC) -- время создания (UTC)
created_utc INTEGER NOT NULL, created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- длина исходного файла в байтах -- длина исходного файла в байтах
source_length INTEGER NOT NULL, source_length INTEGER NOT NULL,
-- UID пользователя (эффективный) -- UID пользователя (эффективный)
@ -69,7 +69,7 @@ auto _scheme = [
-- RUID пользователя (реальный) -- RUID пользователя (реальный)
ruid INTEGER NOT NULL, ruid INTEGER NOT NULL,
-- Процесс -- Процесс
process BLOB NOT NULL, process INTEGER NOT NULL,
-- FastCDC: минимальный размер чанка -- FastCDC: минимальный размер чанка
algo_min INTEGER NOT NULL, algo_min INTEGER NOT NULL,
-- FastCDC: целевой размер чанка -- FastCDC: целевой размер чанка
@ -100,7 +100,7 @@ auto _scheme = [
REFERENCES files(id) REFERENCES files(id)
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE
) WITHOUT ROWID )
}, },
q{ q{
-- ------------------------------------------------------------ -- ------------------------------------------------------------
@ -118,9 +118,9 @@ auto _scheme = [
-- байты (сжатые при zstd=1, иначе исходные) -- байты (сжатые при zstd=1, иначе исходные)
content BLOB NOT NULL, content BLOB NOT NULL,
-- время создания записи (UTC) -- время создания записи (UTC)
created_utc INTEGER NOT NULL, created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- время последней ссылки (UTC) -- время последней ссылки (UTC)
last_seen_utc INTEGER NOT NULL, last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- число ссылок из snapshot_chunks -- число ссылок из snapshot_chunks
refcount INTEGER NOT NULL DEFAULT 0, refcount INTEGER NOT NULL DEFAULT 0,
-- 0=нет сжатия, 1=zstd -- 0=нет сжатия, 1=zstd
@ -132,7 +132,7 @@ auto _scheme = [
(zstd = 0 AND length(content) = size) (zstd = 0 AND length(content) = size)
), ),
CHECK (z_sha256 IS NULL OR length(z_sha256) = 32) CHECK (z_sha256 IS NULL OR length(z_sha256) = 32)
) WITHOUT ROWID )
}, },
q{ q{
-- ------------------------------------------------------------ -- ------------------------------------------------------------
@ -140,7 +140,7 @@ auto _scheme = [
-- ------------------------------------------------------------ -- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS snapshot_chunks ( CREATE TABLE IF NOT EXISTS snapshot_chunks (
-- FK -> snapshots.id -- FK -> snapshots.id
snapshot_id BLOB NOT NULL, snapshot_id INTEGER NOT NULL,
-- порядковый номер чанка в снимке -- порядковый номер чанка в снимке
chunk_index INTEGER NOT NULL, chunk_index INTEGER NOT NULL,
-- смещение чанка в исходном файле, байт -- смещение чанка в исходном файле, байт
@ -156,7 +156,7 @@ auto _scheme = [
REFERENCES blobs(sha256) REFERENCES blobs(sha256)
ON UPDATE RESTRICT ON UPDATE RESTRICT
ON DELETE RESTRICT ON DELETE RESTRICT
) WITHOUT ROWID )
}, },
q{ q{
-- Индекс для запросов вида: WHERE file=? AND sha256=? -- Индекс для запросов вида: WHERE file=? AND sha256=?
@ -178,7 +178,7 @@ auto _scheme = [
BEGIN BEGIN
UPDATE blobs UPDATE blobs
SET refcount = refcount + 1, SET refcount = refcount + 1,
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int) last_seen_utc = CURRENT_TIMESTAMP
WHERE sha256 = NEW.sha256; WHERE sha256 = NEW.sha256;
END END
}, },
@ -213,7 +213,7 @@ auto _scheme = [
UPDATE blobs UPDATE blobs
SET refcount = refcount + 1, SET refcount = refcount + 1,
last_seen_utc = cast(unixepoch("subsecond") * 1000 as int) last_seen_utc = CURRENT_TIMESTAMP
WHERE sha256 = NEW.sha256; WHERE sha256 = NEW.sha256;
END END
}, },

View file

@ -1,21 +1,42 @@
module cdcdb.snapshot; module cdcdb.snapshot;
import cdcdb.dblite; import cdcdb.dblite;
import cdcdb.lib;
import zstd : uncompress; import zstd : uncompress;
import std.digest.sha : SHA256, digest; import std.digest.sha : SHA256, digest;
import std.datetime : DateTime;
import std.exception : enforce; import std.exception : enforce;
import std.datetime : SysTime;
/**
* Чтение снимка и управление его жизненным циклом.
*
* Класс собирает полный файл из чанков, хранящихся через `DBLite`,
* проверяет целостность (SHA-256 каждого чанка и итогового файла)
* и предоставляет безопасное удаление записи о снимке.
*
* Пример:
* ---
* auto s1 = new Snapshot(db, snapshotId);
* auto bytes = s1.data(); // материализовать весь контент в память
*
* // или потоково, без крупной аллокации:
* s1.data((const(ubyte)[] part) {
* // обработать part
* });
* ---
*
* Заметки:
* - Все проверки целостности обязательны; любое расхождение вызывает исключение.
* - Для очень больших файлов предпочтительнее потоковый вариант с делегатом.
*/
final class Snapshot final class Snapshot
{ {
private: private:
DBLite _db; DBLite _db;
DBSnapshot _snapshot; DBSnapshot _snapshot;
// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша. /// Возвращает исходные байты чанка с учётом возможного сжатия и проверкой хеша.
const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk) const(ubyte)[] getBytes(const ref DBSnapshotChunkData chunk)
{ {
ubyte[] bytes; ubyte[] bytes;
@ -36,8 +57,36 @@ private:
} }
public: public:
this(DBLite dblite, DBSnapshot dbSnapshot) { _db = dblite; _snapshot = dbSnapshot; } /// Создать `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() ubyte[] data()
{ {
auto chunks = _db.getChunks(_snapshot.id); auto chunks = _db.getChunks(_snapshot.id);
@ -58,6 +107,15 @@ public:
return content; return content;
} }
/// Потоково передаёт содержимое файла в заданный приёмник.
///
/// Избегает одной большой аллокации: чанк декодируется, проверяется
/// и передаётся в `sink` по порядку.
///
/// Параметры:
/// sink = делегат, вызываемый для каждого проверенного чанка.
///
/// Бросает: Exception при любой ошибке целостности.
void data(void delegate(const(ubyte)[]) sink) void data(void delegate(const(ubyte)[]) sink)
{ {
auto chunks = _db.getChunks(_snapshot.id); auto chunks = _db.getChunks(_snapshot.id);
@ -73,46 +131,154 @@ public:
enforce(_snapshot.sha256 == fctx.finish(), "Хеш итогового файла не совпадает"); 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). /// ID снимка (PRIMARY KEY).
@property Identifier id() nothrow @safe { return _snapshot.id; } @property long id() const nothrow @safe
{
return _snapshot.id;
}
/// Имя файла (из таблицы `files`). /// Имя файла (из таблицы `files`).
@property string file() const nothrow @safe { return _snapshot.file.path; } @property string file() const nothrow @safe
{
return _snapshot.file;
}
/// Время создания (UTC). /// Время создания (UTC).
@property const(SysTime) created() const @safe { return _snapshot.createdUtc.sys; } @property DateTime created() const @safe
{
return _snapshot.createdUtc;
}
/// Длина исходного файла (байты). /// Длина исходного файла (байты).
@property long length() const nothrow @safe { return _snapshot.sourceLength; } @property long length() const nothrow @safe
{
return _snapshot.sourceLength;
}
/// Ожидаемый SHA-256 всего файла (сырые 32 байта). /// Ожидаемый 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: минимальный размер чанка. /// FastCDC: минимальный размер чанка.
@property long algoMin() const nothrow @safe { return _snapshot.algoMin; } @property long algoMin() const nothrow @safe
{
return _snapshot.algoMin;
}
/// FastCDC: целевой (нормальный) размер чанка. /// FastCDC: целевой (нормальный) размер чанка.
@property long algoNormal() const nothrow @safe { return _snapshot.algoNormal; } @property long algoNormal() const nothrow @safe
{
return _snapshot.algoNormal;
}
/// FastCDC: максимальный размер чанка. /// FastCDC: максимальный размер чанка.
@property long algoMax() const nothrow @safe { return _snapshot.algoMax; } @property long algoMax() const nothrow @safe
{
return _snapshot.algoMax;
}
/// FastCDC: строгая маска. /// FastCDC: строгая маска.
@property long maskS() const nothrow @safe { return _snapshot.maskS; } @property long maskS() const nothrow @safe
{
return _snapshot.maskS;
}
/// FastCDC: слабая маска. /// FastCDC: слабая маска.
@property long maskL() const nothrow @safe { return _snapshot.maskL; } @property long maskL() const nothrow @safe
{
return _snapshot.maskL;
}
/// UID процесса (effective). /// UID процесса (effective).
@property long uid() const nothrow @safe { return _snapshot.uid; } @property long uid() const nothrow @safe
{
return _snapshot.uid;
}
/// Real UID процесса. /// Real UID процесса.
@property long ruid() const nothrow @safe { return _snapshot.ruid; } @property long ruid() const nothrow @safe
{
return _snapshot.ruid;
}
/// Имя пользователя для `uid`. /// Имя пользователя для `uid`.
@property string uidName() const nothrow @safe { return _snapshot.uidName; } @property string uidName() const nothrow @safe
{
return _snapshot.uidName;
}
/// Имя пользователя для `ruid`. /// Имя пользователя для `ruid`.
@property string ruidName() const nothrow @safe { return _snapshot.ruidName; } @property string ruidName() const nothrow @safe
{
return _snapshot.ruidName;
}
/// Имя процесса (из таблицы `processes`). /// Имя процесса (из таблицы `processes`).
@property string process() const nothrow @safe { return _snapshot.process.name; } @property string process() const nothrow @safe
{
return _snapshot.process;
}
/// Удобный флаг: снимок «готов». /// Удобный флаг: снимок «готов».
@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;
}
} }

View file

@ -1,15 +1,12 @@
module cdcdb.storage; module cdcdb.storage;
import cdcdb.dblite; import cdcdb.dblite;
import cdcdb.storagefile;
import cdcdb.snapshot;
import cdcdb.core; import cdcdb.core;
import cdcdb.lib : Identifier; import cdcdb.snapshot;
import zstd : compress, Level; import zstd : compress, Level;
import std.exception : enforce; /// Контекст создания снимка (идентификаторы и процесс).
struct Context struct Context
{ {
long uid; /// UID процесса (effective). long uid; /// UID процесса (effective).
@ -19,6 +16,27 @@ struct Context
string process; /// Имя процесса. 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 final class Storage
{ {
private: private:
@ -48,6 +66,14 @@ private:
} }
public: public:
/// Конструктор: открывает/создаёт БД и подготавливает фасад.
///
/// Параметры:
/// database = путь к файлу SQLite
/// zstd = включить Zstd-сжатие для блобов
/// level = уровень сжатия (см. `zstd.Level`)
/// busyTimeout = таймаут ожидания блокировки SQLite (мс)
/// maxRetries = число повторов при SQLITE_BUSY/LOCKED
this(string database, bool zstd = false, int level = Level.base, this(string database, bool zstd = false, int level = Level.base,
size_t busyTimeout = 3000, size_t maxRetries = 3) size_t busyTimeout = 3000, size_t maxRetries = 3)
{ {
@ -57,27 +83,44 @@ public:
initCDC(); initCDC();
} }
/// Перенастроить параметры CDC (влияет на будущие снимки).
void setupCDC(size_t minSize, size_t normalSize, size_t maxSize, void setupCDC(size_t minSize, size_t normalSize, size_t maxSize,
size_t maskS, size_t maskL) size_t maskS, size_t maskL)
{ {
initCDC(minSize, normalSize, maxSize, maskS, maskL); initCDC(minSize, normalSize, maxSize, maskS, maskL);
} }
Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init) /// Создаёт новый снимок из массива байт.
///
/// - Разбивает данные по текущим параметрам 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)
{ {
enforce(data.length > 0, "Данные имеют нулевую длину"); if (data.length == 0)
{
auto dbFile = _db.getFile(file); throw new Exception("Данные имеют нулевую длину");
}
import std.digest.sha : SHA256, digest; import std.digest.sha : SHA256, digest;
ubyte[32] sha256 = digest!SHA256(data); ubyte[32] sha256 = digest!SHA256(data);
if (dbFile.empty) { // Если последний снимок совпадает — пропустить
dbFile = _db.addFile(file); if (_db.isLast(file, sha256))
} else if (_db.isLast(dbFile.id, sha256)) {
return null; return null;
}
_db.beginImmediate(); _db.beginImmediate();
bool ok; bool ok;
@ -92,22 +135,18 @@ public:
_db.commit(); _db.commit();
} }
// Запись пользователей/файлов/процессов
_db.addUser(context.uid, context.uidName); _db.addUser(context.uid, context.uidName);
if (context.uid != context.ruid) if (context.uid != context.ruid)
{ {
_db.addUser(context.ruid, context.ruidName); _db.addUser(context.ruid, context.ruidName);
} }
_db.addFile(file);
auto dbProcess = _db.getProcess(context.process); _db.addProcess(context.process);
if (dbProcess.empty) {
dbProcess = _db.addProcess(context.process);
}
// Метаданные снимка // Метаданные снимка
DBSnapshot dbSnapshot; DBSnapshot dbSnapshot;
dbSnapshot.file = file;
dbSnapshot.file = dbFile;
dbSnapshot.sha256 = sha256; dbSnapshot.sha256 = sha256;
dbSnapshot.description = description; dbSnapshot.description = description;
dbSnapshot.sourceLength = data.length; dbSnapshot.sourceLength = data.length;
@ -118,9 +157,9 @@ public:
dbSnapshot.maskL = _maskL; dbSnapshot.maskL = _maskL;
dbSnapshot.uid = context.uid; dbSnapshot.uid = context.uid;
dbSnapshot.ruid = context.ruid; dbSnapshot.ruid = context.ruid;
dbSnapshot.process = dbProcess; dbSnapshot.process = context.process;
enforce(_db.addSnapshot(dbSnapshot), "Не удалось добавить новый снимок в базу данных"); auto idSnapshot = _db.addSnapshot(dbSnapshot);
// Чанки и блобы // Чанки и блобы
DBSnapshotChunk dbSnapshotChunk; DBSnapshotChunk dbSnapshotChunk;
@ -153,99 +192,54 @@ public:
_db.addBlob(dbBlob); _db.addBlob(dbBlob);
dbSnapshotChunk.snapshotId = dbSnapshot.id; dbSnapshotChunk.snapshotId = idSnapshot;
dbSnapshotChunk.chunkIndex = chunk.index; dbSnapshotChunk.chunkIndex = chunk.index;
dbSnapshotChunk.offset = chunk.offset; dbSnapshotChunk.offset = chunk.offset;
dbSnapshotChunk.sha256 = chunk.sha256; dbSnapshotChunk.sha256 = chunk.sha256;
enforce(_db.addSnapshotChunk(dbSnapshotChunk), "Не удалось привязать снимок к данным"); _db.addSnapshotChunk(dbSnapshotChunk);
} }
ok = true; ok = true;
return new Snapshot(_db, dbSnapshot); return new Snapshot(_db, idSnapshot);
} }
StorageFile getFile(string path) { /// Удаляет все снимки по имени файла.
auto dbFile = _db.getFile(path); long removeSnapshots(string file)
if (dbFile.empty) return null;
return new StorageFile(_db, dbFile);
}
StorageFile getFile(Identifier id) {
auto dbFile = _db.getFile(id);
if (dbFile.empty) return null;
return new StorageFile(_db, dbFile);
}
StorageFile[] getFiles() {
StorageFile[] storageFiles;
foreach (dbFile; _db.getFiles()) {
storageFiles ~= new StorageFile(_db, dbFile);
}
return storageFiles;
}
StorageFile[] findFile(string pattern) {
StorageFile[] storageFiles;
foreach (dbFile; _db.findFile(pattern)) {
storageFiles ~= new StorageFile(_db, dbFile);
}
return storageFiles;
}
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 (dbSnapshot; _db.getSnapshots(id))
{ {
snapshots ~= new Snapshot(_db, dbSnapshot); return _db.deleteSnapshots(file);
}
return snapshots;
} }
Snapshot[] getSnapshots(string file) { /// Удаляет конкретный снимок по объекту `Snapshot`.
Snapshot[] snapshots; bool removeSnapshot(Snapshot snapshot)
foreach (dbSnapshot; _db.getSnapshots(file))
{ {
snapshots ~= new Snapshot(_db, dbSnapshot); return removeSnapshot(snapshot.id);
}
return snapshots;
} }
Snapshot[] findSnapshot(Identifier 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; Snapshot[] snapshots;
foreach (dbSnapshot; _db.findSnapshot(id)) { foreach (snapshot; _db.getSnapshots(file))
snapshots ~= new Snapshot(_db, dbSnapshot); {
snapshots ~= new Snapshot(_db, snapshot);
} }
return snapshots; return snapshots;
} }
bool deleteFile(Identifier id) { /// Версия библиотеки.
return _db.deleteFile(id);
}
bool deleteFile(string name) {
return _db.deleteFile(name);
}
bool deleteSnapshot(Identifier id) {
return _db.deleteSnapshot(id);
}
string getVersion() const @safe nothrow string getVersion() const @safe nothrow
{ {
import cdcdb.version_ : cdcdbVersion; import cdcdb.version_ : cdcdbVersion;

View file

@ -1,25 +0,0 @@
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;
}
}

View file

@ -1,3 +1,3 @@
module cdcdb.version_; module cdcdb.version_;
enum cdcdbVersion = "0.2.0"; enum cdcdbVersion = "0.1.0";

View file

@ -3,14 +3,14 @@ import std.stdio : writeln, File;
import std.file : exists, remove, read; import std.file : exists, remove, read;
import zstd : Level; import zstd : Level;
import core.thread : Thread, msecs, dur;
void main() void main()
{ {
// Создаем временную базу для примера // Создаем временную базу для примера
string dbPath = "./bin/example.db"; string dbPath = "./bin/example.db";
if (exists(dbPath)) { remove(dbPath); } if (exists(dbPath)) {
remove(dbPath);
}
Context context; Context context;
@ -31,18 +31,15 @@ void main()
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup; ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
// Создание первого снимка // Создание первого снимка
Snapshot snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0"); auto snap1 = storage.newSnapshot("example_file", data1, context, "Версия 1.0");
if (snap1) if (snap1)
{ {
writeln("Создан снимок с ID: ", snap1.id); writeln("Создан снимок с ID: ", snap1.id);
writeln("Файл: ", snap1.file); writeln("Файл: ", snap1.file);
writeln("Размер: ", snap1.length, " байт"); writeln("Размер: ", snap1.length, " байт");
writeln("Статус: ", snap1.status); writeln("Статус: ", snap1.status);
writeln("Время: ", snap1.created);
} }
Thread.sleep( dur!("msecs")( 50 ) );
// Создание второго снимка (обновление) // Создание второго снимка (обновление)
auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0"); auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
if (snap2) if (snap2)
@ -71,9 +68,9 @@ void main()
writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData)); writeln("Хэш совпадает: ", lastSnap.sha256 == digest!SHA256(restoredData));
} }
// Удаление файла // Удаление снимков по метке
if (storage.deleteFile("example_file")) long deleted = storage.removeSnapshots("example_file");
writeln("Файл example_file удален."); writeln("Удалено снимков: ", deleted);
// Проверка: снимки удалены // Проверка: снимки удалены
auto remaining = storage.getSnapshots("example_file"); auto remaining = storage.getSnapshots("example_file");