1
0
Fork 0
forked from dlang/cdcdb

Compare commits

...
Sign in to create a new pull request.

1 commit
master ... mod

Author SHA1 Message Date
1f50b21457
Библиотека модернизирована под использование в БД в качестве первичных ключей UUID идентификаторов.
Время фиксируется в Unix Timestamp метке с миллисекундами.
Добавлена новая сущеность StorageFile, который является отправной точкой для работы со снимками.
Расширен интерфейс Storage - добавлены дополнительные фозможности для работы со снимками.
Введены структуры: для работы со временм - UTS, для работы с идентификатором - Identifier.
2025-09-30 02:39:29 +03:00
12 changed files with 989 additions and 602 deletions

2
.gitignore vendored
View file

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

File diff suppressed because it is too large Load diff

98
source/cdcdb/lib/hash.d Normal file
View file

@ -0,0 +1,98 @@
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(ref const ubyte[16] 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

@ -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
View 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;
}
}

View file

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

View file

@ -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
},

View file

@ -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; }
}

View file

@ -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;

View 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;
}
}

View file

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

View file

@ -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,9 +71,9 @@ 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");