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
2
.gitignore
vendored
|
|
@ -15,4 +15,4 @@ cdcdb-test-*
|
||||||
*.obj
|
*.obj
|
||||||
*.lst
|
*.lst
|
||||||
bin
|
bin
|
||||||
/lib
|
lib
|
||||||
|
|
|
||||||
2
dub.json
2
dub.json
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
module cdcdb.lib;
|
|
||||||
|
|
||||||
public import cdcdb.lib.hash;
|
|
||||||
public import cdcdb.lib.uts;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
return _db.deleteSnapshots(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageFile getFile(Identifier id) {
|
/// Удаляет конкретный снимок по объекту `Snapshot`.
|
||||||
auto dbFile = _db.getFile(id);
|
bool removeSnapshot(Snapshot snapshot)
|
||||||
if (dbFile.empty) return null;
|
{
|
||||||
return new StorageFile(_db, dbFile);
|
return removeSnapshot(snapshot.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageFile[] getFiles() {
|
/// Удаляет снимок по id.
|
||||||
StorageFile[] storageFiles;
|
bool removeSnapshot(long idSnapshot)
|
||||||
foreach (dbFile; _db.getFiles()) {
|
{
|
||||||
storageFiles ~= new StorageFile(_db, dbFile);
|
return _db.deleteSnapshot(idSnapshot) == idSnapshot;
|
||||||
}
|
|
||||||
return storageFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageFile[] findFile(string pattern) {
|
/// Возвращает `Snapshot` по id.
|
||||||
StorageFile[] storageFiles;
|
Snapshot getSnapshot(long idSnapshot)
|
||||||
foreach (dbFile; _db.findFile(pattern)) {
|
{
|
||||||
storageFiles ~= new StorageFile(_db, dbFile);
|
return new Snapshot(_db, idSnapshot);
|
||||||
}
|
|
||||||
return storageFiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageFile[] findFile(Identifier id) {
|
/// Возвращает список снимков (опционально фильтр по имени файла).
|
||||||
StorageFile[] storageFiles;
|
Snapshot[] getSnapshots(string file = string.init)
|
||||||
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;
|
Snapshot[] snapshots;
|
||||||
foreach (dbSnapshot; _db.getSnapshots(id))
|
foreach (snapshot; _db.getSnapshots(file))
|
||||||
{
|
{
|
||||||
snapshots ~= new Snapshot(_db, dbSnapshot);
|
snapshots ~= new Snapshot(_db, snapshot);
|
||||||
}
|
}
|
||||||
return snapshots;
|
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
|
string getVersion() const @safe nothrow
|
||||||
{
|
{
|
||||||
import cdcdb.version_ : cdcdbVersion;
|
import cdcdb.version_ : cdcdbVersion;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
module cdcdb.version_;
|
module cdcdb.version_;
|
||||||
|
|
||||||
enum cdcdbVersion = "0.2.0";
|
enum cdcdbVersion = "0.1.0";
|
||||||
|
|
|
||||||
17
test/app.d
17
test/app.d
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue