1
0
Fork 0
forked from dlang/cdcdb

Обновлена схема БД - добавлены таблицы users, processes, files. Добавлен параметр Context с идентификацией пользователей и процесса. label сменен на file.

This commit is contained in:
Alexander Zhirov 2025-09-14 00:30:27 +03:00
parent 5bb4d65c92
commit 49ee7a4053
Signed by: alexander
GPG key ID: C8D8BE544A27C511
7 changed files with 289 additions and 168 deletions

View file

@ -1,12 +0,0 @@
# Changelog
## [0.1.0] — 2025-09-13
### Added
- Библиотека для снимков данных на базе SQLite с контентно-зависимым разбиением (FastCDC).
- Дедупликация по SHA-256 чанков, опциональная компрессия Zstd.
- Сквозная проверка целостности: хеш каждого чанка и итогового файла.
- Транзакции (WAL), базовые ограничения целостности и триггеры.
- Высокоуровневый API:
- `Storage`: `newSnapshot`, `getSnapshots`, `getSnapshot`, `removeSnapshots`, `setupCDC`, `getVersion`.
- `Snapshot`: `data()` (буфер) и потоковый `data(void delegate(const(ubyte)[]))`, `remove()`, свойства (`id`, `label`, `created`, `length`, `sha256`, `status`, `description`).
- Инструмент для генерации Gear-таблицы для FastCDC (`tools/gen.d`).

View file

@ -1,98 +0,0 @@
# cdcdb
Библиотека для хранения и управления снимками текстовых данных в базе SQLite. Использует механизм content-defined chunking (CDC) на основе алгоритма FastCDC для разделения данных на чанки переменного размера, что обеспечивает эффективную дедупликацию. Поддерживает опциональную компрессию Zstd, транзакции и проверку целостности данных через SHA-256. Основное применение — резервное копирование и версионирование текстовых файлов с минимизацией занимаемого пространства.
## Алгоритм FastCDC
FastCDC — это алгоритм разделения данных на чанки переменного размера, основанный на хэшировании содержимого. Он использует таблицу Gear для вычисления "отпечатков" данных, определяя точки разделения с учетом минимального, целевого и максимального размеров чанка. Это позволяет эффективно выявлять изменения в данных и хранить только уникальные чанки, снижая объем хранилища.
## Основные классы
### Storage
Класс для работы с базой SQLite и управления снимками.
- **Конструктор**: Инициализирует подключение к базе SQLite.
- **Методы**:
- `newSnapshot`: Создает снимок данных. Возвращает объект `Snapshot` или `null`, если данные совпадают с последним снимком.
- `getSnapshots`: Получает список снимков (все или по метке). Возвращает массив объектов `Snapshot`.
- `getSnapshot`: Получает снимок по ID. Возвращает объект `Snapshot`.
- `setupCDC`: Настраивает параметры разделения данных CDC. Ничего не возвращает.
- `removeSnapshots`: Удаляет снимки по метке, ID или объекту Snapshot. Возвращает количество удаленных снимков (для метки) или `true`/`false` (для ID или объекта).
- `getVersion`: Возвращает строку с версией библиотеки.
### Snapshot
Класс для работы с отдельным снимком данных.
- **Конструктор**: Создает объект снимка для работы с данными по его ID.
- **Методы**:
- `data`: Извлекает полные данные снимка. Возвращает массив байтов (`ubyte[]`).
- `data`: Извлекает данные снимка потоково через делегат. Ничего не возвращает.
- `remove`: Удаляет снимок из базы. Возвращает `true` при успешном удалении, иначе `false`.
- **Свойства**:
- `id`: ID снимка (long).
- `label`: Метка снимка (string).
- `created`: Временная метка создания (UTC, `DateTime`).
- `length`: Длина исходных данных (long).
- `sha256`: Хэш SHA-256 данных (ubyte[32]).
- `status`: Статус снимка ("pending" или "ready").
- `description`: Описание снимка (string).
## Пример использования
```d
import cdcdb;
import std.stdio : writeln, File;
import std.file : exists, remove;
void main()
{
// Создаем базу
string dbPath = "example.db";
// Инициализация Storage с компрессией Zstd
auto storage = new Storage(dbPath, true, 22);
// Создание снимка
ubyte[] data = cast(ubyte[]) "Hello, cdcdb!".dup;
auto snap = storage.newSnapshot("example_file", data, "Версия 1.0");
if (snap)
{
writeln("Создан снимок: ID=", snap.id, ", Метка=", snap.label);
}
// Восстановление данных
auto snapshots = storage.getSnapshots("example_file");
if (snapshots.length > 0)
{
auto lastSnap = snapshots[0];
File outFile = File("restored.txt", "wb");
lastSnap.data((const(ubyte)[] chunk) => outFile.rawWrite(chunk));
outFile.close();
writeln("Данные восстановлены в restored.txt");
}
// Удаление снимков
long deleted = storage.removeSnapshots("example_file");
writeln("Удалено снимков: ", deleted);
}
```
## Инструменты
В директории `tools` находится скрипт на D для создания таблицы Gear, используемой в алгоритме FastCDC. Он позволяет генерировать пользовательские таблицы хэшей для настройки разделения данных. Для создания новой таблицы выполните:
```bash
chmod +x ./tools/gen.d
./tools/gen.d > ./source/gear.d
```
## Установка
- **В `dub.json`**:
```json
"dependencies": {
"cdcdb": "~>0.1.0"
}
```
- **Сборка**: `dub build`.
## Лицензия
Boost Software License 1.0 (BSL-1.0).

View file

@ -17,7 +17,7 @@ enum SnapshotStatus : ubyte
struct DBSnapshot { struct DBSnapshot {
long id; long id;
string label; string file;
ubyte[32] sha256; ubyte[32] sha256;
string description; string description;
DateTime createdUtc; DateTime createdUtc;
@ -28,6 +28,11 @@ struct DBSnapshot {
long maskS; long maskS;
long maskL; long maskL;
SnapshotStatus status; SnapshotStatus status;
long uid;
long ruid;
string uidName;
string ruidName;
string process;
} }
struct DBSnapshotChunk struct DBSnapshotChunk
@ -104,7 +109,8 @@ private:
{ {
SqliteResult queryResult = sql( SqliteResult queryResult = sql(
q{ q{
WITH required(name) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks")) WITH required(name)
AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files"))
SELECT name AS missing_table SELECT name AS missing_table
FROM required FROM required
WHERE NOT EXISTS ( WHERE NOT EXISTS (
@ -122,11 +128,11 @@ private:
missingTables ~= row["missing_table"].to!string; missingTables ~= row["missing_table"].to!string;
} }
enforce(missingTables.length == 0 || missingTables.length == 3, enforce(missingTables.length == 0 || missingTables.length == 6,
"Database is corrupted. Missing tables: " ~ missingTables.join(", ") "Database is corrupted. Missing tables: " ~ missingTables.join(", ")
); );
if (missingTables.length == 3) if (missingTables.length == 6)
{ {
foreach (schemeQuery; _scheme) foreach (schemeQuery; _scheme)
{ {
@ -172,17 +178,21 @@ public:
sql("ROLLBACK"); sql("ROLLBACK");
} }
bool isLast(string label, ubyte[] sha256) { bool isLast(string file, ubyte[] sha256) {
auto queryResult = sql( auto queryResult = sql(
q{ q{
SELECT COALESCE( SELECT COALESCE(
(SELECT (label = ? AND sha256 = ?) (
FROM snapshots SELECT (s.sha256 = ?2)
ORDER BY created_utc DESC FROM snapshots s
LIMIT 1), JOIN files f ON f.id = s.file
WHERE f.name = ?1
ORDER BY s.created_utc DESC
LIMIT 1
),
0 0
) AS is_last; ) AS is_last;
}, label, sha256 }, file, sha256
); );
if (!queryResult.empty()) if (!queryResult.empty())
@ -195,23 +205,34 @@ public:
auto queryResult = sql( auto queryResult = sql(
q{ q{
INSERT INTO snapshots( INSERT INTO snapshots(
label, file,
sha256, sha256,
description, description,
source_length, source_length,
uid,
ruid,
process,
algo_min, algo_min,
algo_normal, algo_normal,
algo_max, algo_max,
mask_s, mask_s,
mask_l, mask_l,
status status
) VALUES (?,?,?,?,?,?,?,?,?,?) )
SELECT
(SELECT id FROM files WHERE name = ?),
?,?,?,?,?,
(SELECT id FROM processes WHERE name = ?),
?,?,?,?,?,?
RETURNING id RETURNING id
}, },
snapshot.label, snapshot.file,
snapshot.sha256[], snapshot.sha256[],
snapshot.description.length ? snapshot.description : null, snapshot.description.length ? snapshot.description : null,
snapshot.sourceLength, snapshot.sourceLength,
snapshot.uid,
snapshot.ruid,
snapshot.process,
snapshot.algoMin, snapshot.algoMin,
snapshot.algoNormal, snapshot.algoNormal,
snapshot.algoMax, snapshot.algoMax,
@ -247,6 +268,43 @@ public:
return !queryResult.empty(); return !queryResult.empty();
} }
bool addProcess(string name)
{
auto queryResult = sql(
q{
INSERT INTO processes (name) VALUES (?)
ON CONFLICT(name) DO NOTHING
}, name
);
return !queryResult.empty();
}
bool addFile(string name)
{
auto queryResult = sql(
q{
INSERT INTO files (name) VALUES (?)
ON CONFLICT(name) DO NOTHING
}, name
);
return !queryResult.empty();
}
bool addUser(long uid, string name)
{
auto queryResult = sql(
q{
INSERT INTO users (uid, name)
VALUES (?, ?)
ON CONFLICT(uid) DO NOTHING;
}, uid, name
);
return !queryResult.empty();
}
bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
{ {
auto queryResult = sql( auto queryResult = sql(
@ -268,9 +326,30 @@ public:
{ {
auto queryResult = sql( auto queryResult = sql(
q{ q{
SELECT id, label, sha256, description, created_utc, source_length, SELECT
algo_min, algo_normal, algo_max, mask_s, mask_l, status s.id,
FROM snapshots WHERE id = ? f.name file,
s.sha256,
s.description,
s.created_utc,
s.source_length,
s.uid,
s.ruid,
u.name uid_name,
r.name ruid_name,
p.name process,
s.algo_min,
s.algo_normal,
s.algo_max,
s.mask_s,
s.mask_l,
s.status
FROM snapshots s
JOIN processes p ON p.id = s.process
JOIN users u ON u.uid = s.uid
JOIN users r ON r.uid = s.ruid
JOIN files f ON f.id = s.file
WHERE s.id = ?;
}, id }, id
); );
@ -281,7 +360,7 @@ public:
auto data = queryResult.front(); auto data = queryResult.front();
snapshot.id = data["id"].to!long; snapshot.id = data["id"].to!long;
snapshot.label = data["label"].to!string; snapshot.file = data["file"].to!string;
snapshot.sha256 = cast(ubyte[]) data["sha256"].dup; snapshot.sha256 = cast(ubyte[]) data["sha256"].dup;
snapshot.description = data["description"].to!string; snapshot.description = data["description"].to!string;
snapshot.createdUtc = toDateTime(data["created_utc"].to!string); snapshot.createdUtc = toDateTime(data["created_utc"].to!string);
@ -292,19 +371,45 @@ public:
snapshot.maskS = data["mask_s"].to!long; snapshot.maskS = data["mask_s"].to!long;
snapshot.maskL = data["mask_l"].to!long; snapshot.maskL = data["mask_l"].to!long;
snapshot.status = cast(SnapshotStatus) data["status"].to!int; snapshot.status = cast(SnapshotStatus) data["status"].to!int;
snapshot.uid = data["uid"].to!long;
snapshot.ruid = data["ruid"].to!long;
snapshot.uidName = data["uid_name"].to!string;
snapshot.ruidName = data["ruid_name"].to!string;
snapshot.process = data["process"].to!string;
} }
return snapshot; return snapshot;
} }
DBSnapshot[] getSnapshots(string label) DBSnapshot[] getSnapshots(string file)
{ {
auto queryResult = sql( auto queryResult = sql(
q{ q{
SELECT id, label, sha256, description, created_utc, source_length, SELECT
algo_min, algo_normal, algo_max, mask_s, mask_l, status s.id,
FROM snapshots WHERE (length(?) = 0 OR label = ?1); f.name file,
}, label s.sha256,
s.description,
s.created_utc,
s.source_length,
s.uid,
s.ruid,
u.name uid_name,
r.name ruid_name,
p.name process,
s.algo_min,
s.algo_normal,
s.algo_max,
s.mask_s,
s.mask_l,
s.status
FROM snapshots s
JOIN processes p ON p.id = s.process
JOIN users u ON u.uid = s.uid
JOIN users r ON r.uid = s.ruid
JOIN files f ON f.id = s.file AND (length(?) = 0 OR f.name = ?1)
ORDER BY s.created_utc, s.id;
}, file
); );
DBSnapshot[] snapshots; DBSnapshot[] snapshots;
@ -314,7 +419,7 @@ public:
DBSnapshot snapshot; DBSnapshot snapshot;
snapshot.id = row["id"].to!long; snapshot.id = row["id"].to!long;
snapshot.label = row["label"].to!string; snapshot.file = row["file"].to!string;
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; snapshot.sha256 = cast(ubyte[]) row["sha256"].dup;
snapshot.description = row["description"].to!string; snapshot.description = row["description"].to!string;
snapshot.createdUtc = toDateTime(row["created_utc"].to!string); snapshot.createdUtc = toDateTime(row["created_utc"].to!string);
@ -325,6 +430,11 @@ public:
snapshot.maskS = row["mask_s"].to!long; snapshot.maskS = row["mask_s"].to!long;
snapshot.maskL = row["mask_l"].to!long; snapshot.maskL = row["mask_l"].to!long;
snapshot.status = cast(SnapshotStatus) row["status"].to!int; snapshot.status = cast(SnapshotStatus) row["status"].to!int;
snapshot.uid = row["uid"].to!long;
snapshot.ruid = row["ruid"].to!long;
snapshot.uidName = row["uid_name"].to!string;
snapshot.ruidName = row["ruid_name"].to!string;
snapshot.process = row["process"].to!string;
snapshots ~= snapshot; snapshots ~= snapshot;
} }
@ -375,13 +485,13 @@ public:
return 0; return 0;
} }
long deleteSnapshot(string label) { long deleteSnapshots(string file) {
auto queryResult = sql( auto queryResult = sql(
q{ q{
DELETE FROM snapshots DELETE FROM snapshots
WHERE label = ? WHERE file = (SELECT id FROM files WHERE name = ?)
RETURNING 1; RETURNING 1;
}, label); }, file);
if (!queryResult.empty()) { if (!queryResult.empty()) {
return queryResult.length.to!long; return queryResult.length.to!long;

View file

@ -1,4 +1,52 @@
auto _scheme = [ auto _scheme = [
q{
-- ------------------------------------------------------------
-- Таблица users
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS users (
-- Linux UID
uid INTEGER PRIMARY KEY,
-- текстовое представление пользователя
name TEXT NOT NULL UNIQUE
)
},
q{
-- Индекс по имени пользователя
CREATE INDEX IF NOT EXISTS idx_users_name
ON users(name)
},
q{
-- ------------------------------------------------------------
-- Таблица processes
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS processes (
-- идентификатор процесса
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- имя процесса
name TEXT NOT NULL UNIQUE
)
},
q{
-- Индекс по имени процесса
CREATE INDEX IF NOT EXISTS idx_processes_name
ON processes(name)
},
q{
-- ------------------------------------------------------------
-- Таблица files
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS files (
-- идентификатор процесса
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- имя процесса
name TEXT NOT NULL UNIQUE
)
},
q{
-- Индекс по имени процесса
CREATE INDEX IF NOT EXISTS idx_processes_name
ON processes(name)
},
q{ q{
-- ------------------------------------------------------------ -- ------------------------------------------------------------
-- Таблица snapshots -- Таблица snapshots
@ -6,8 +54,8 @@ auto _scheme = [
CREATE TABLE IF NOT EXISTS snapshots ( CREATE TABLE IF NOT EXISTS snapshots (
-- идентификатор снимка -- идентификатор снимка
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
-- метка/название снимка -- Файл
label TEXT 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),
-- Комментарий/описание -- Комментарий/описание
@ -16,6 +64,12 @@ auto _scheme = [
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- длина исходного файла в байтах -- длина исходного файла в байтах
source_length INTEGER NOT NULL, source_length INTEGER NOT NULL,
-- UID пользователя (эффективный)
uid INTEGER NOT NULL,
-- RUID пользователя (реальный)
ruid INTEGER NOT NULL,
-- Процесс
process INTEGER NOT NULL,
-- FastCDC: минимальный размер чанка -- FastCDC: минимальный размер чанка
algo_min INTEGER NOT NULL, algo_min INTEGER NOT NULL,
-- FastCDC: целевой размер чанка -- FastCDC: целевой размер чанка
@ -28,7 +82,24 @@ auto _scheme = [
mask_l INTEGER NOT NULL, mask_l INTEGER NOT NULL,
-- 0=pending, 1=ready -- 0=pending, 1=ready
status INTEGER NOT NULL DEFAULT 0 status INTEGER NOT NULL DEFAULT 0
CHECK (status IN (0,1)) CHECK (status IN (0,1)),
-- Внешние ключи
FOREIGN KEY (uid)
REFERENCES users(uid)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (ruid)
REFERENCES users(uid)
ON UPDATE RESTRICT
ON DELETE RESTRICT,
FOREIGN KEY (process)
REFERENCES processes(id)
ON UPDATE RESTRICT
ON DELETE RESTRICT
FOREIGN KEY (file)
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE CASCADE
) )
}, },
q{ q{
@ -88,9 +159,9 @@ auto _scheme = [
) )
}, },
q{ q{
-- Индекс для запросов вида: WHERE label=? AND sha256=? -- Индекс для запросов вида: WHERE file=? AND sha256=?
CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha CREATE INDEX IF NOT EXISTS idx_snapshots_path_sha
ON snapshots(label, sha256) ON snapshots(file, sha256)
}, },
q{ q{
-- Индекс для обратного поиска использования blob по sha256 -- Индекс для обратного поиска использования blob по sha256
@ -215,11 +286,14 @@ auto _scheme = [
FOR EACH ROW FOR EACH ROW
WHEN WHEN
NEW.id IS NOT OLD.id OR NEW.id IS NOT OLD.id OR
NEW.label IS NOT OLD.label OR NEW.file IS NOT OLD.file OR
NEW.sha256 IS NOT OLD.sha256 OR NEW.sha256 IS NOT OLD.sha256 OR
NEW.description IS NOT OLD.description OR NEW.description IS NOT OLD.description OR
NEW.created_utc IS NOT OLD.created_utc OR NEW.created_utc IS NOT OLD.created_utc OR
NEW.source_length IS NOT OLD.source_length OR NEW.source_length IS NOT OLD.source_length OR
NEW.uid IS NOT OLD.uid OR
NEW.ruid IS NOT OLD.ruid OR
NEW.process IS NOT OLD.process OR
NEW.algo_min IS NOT OLD.algo_min OR NEW.algo_min IS NOT OLD.algo_min OR
NEW.algo_normal IS NOT OLD.algo_normal OR NEW.algo_normal IS NOT OLD.algo_normal OR
NEW.algo_max IS NOT OLD.algo_max OR NEW.algo_max IS NOT OLD.algo_max OR
@ -249,5 +323,20 @@ auto _scheme = [
BEGIN BEGIN
SELECT RAISE(ABORT, "blobs: разрешён UPDATE только полей last_seen_utc и refcount"); SELECT RAISE(ABORT, "blobs: разрешён UPDATE только полей last_seen_utc и refcount");
END END
},
q{
-- ------------------------------------------------------------
-- Удаление записи из files, если удалён последний snapshot
-- ------------------------------------------------------------
CREATE TRIGGER IF NOT EXISTS trg_snapshots_delete_file
AFTER DELETE ON snapshots
FOR EACH ROW
BEGIN
DELETE FROM files
WHERE id = OLD.file
AND NOT EXISTS (
SELECT 1 FROM snapshots WHERE file = OLD.file
);
END;
} }
]; ];

View file

@ -167,9 +167,9 @@ public:
} }
/// User-defined label. /// User-defined label.
@property string label() const @safe @property string file() const @safe
{ {
return _snapshot.label; return _snapshot.file;
} }
/// Creation timestamp (UTC) from the database. /// Creation timestamp (UTC) from the database.

View file

@ -6,6 +6,14 @@ import cdcdb.snapshot;
import zstd : compress, Level; import zstd : compress, Level;
struct Context {
long uid;
long ruid;
string uidName;
string ruidName;
string process;
}
/** /**
* High-level storage facade: splits data into CDC chunks, stores chunks/blobs * High-level storage facade: splits data into CDC chunks, stores chunks/blobs
* into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot` * into SQLite via `DBLite`, links them into snapshots, and returns `Snapshot`
@ -97,7 +105,7 @@ public:
/// ///
/// Throws: /// Throws:
/// Exception if `data` is empty or on database/storage errors /// Exception if `data` is empty or on database/storage errors
Snapshot newSnapshot(string label, const(ubyte)[] data, string description = string.init) Snapshot newSnapshot(string file, const(ubyte)[] data, Context context, string description = string.init)
{ {
if (data.length == 0) if (data.length == 0)
{ {
@ -109,21 +117,9 @@ public:
ubyte[32] sha256 = digest!SHA256(data); ubyte[32] sha256 = digest!SHA256(data);
// If the last snapshot for the label matches current content // If the last snapshot for the label matches current content
if (_db.isLast(label, sha256)) if (_db.isLast(file, sha256))
return null; return null;
DBSnapshot dbSnapshot;
dbSnapshot.label = label;
dbSnapshot.sha256 = sha256;
dbSnapshot.description = description;
dbSnapshot.sourceLength = data.length;
dbSnapshot.algoMin = _minSize;
dbSnapshot.algoNormal = _normalSize;
dbSnapshot.algoMax = _maxSize;
dbSnapshot.maskS = _maskS;
dbSnapshot.maskL = _maskL;
_db.beginImmediate(); _db.beginImmediate();
bool ok; bool ok;
@ -138,6 +134,30 @@ public:
_db.commit(); _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);
DBSnapshot dbSnapshot;
dbSnapshot.file = file;
dbSnapshot.sha256 = sha256;
dbSnapshot.description = description;
dbSnapshot.sourceLength = data.length;
dbSnapshot.algoMin = _minSize;
dbSnapshot.algoNormal = _normalSize;
dbSnapshot.algoMax = _maxSize;
dbSnapshot.maskS = _maskS;
dbSnapshot.maskL = _maskL;
dbSnapshot.uid = context.uid;
dbSnapshot.ruid = context.ruid;
dbSnapshot.process = context.process;
auto idSnapshot = _db.addSnapshot(dbSnapshot); auto idSnapshot = _db.addSnapshot(dbSnapshot);
DBSnapshotChunk dbSnapshotChunk; DBSnapshotChunk dbSnapshotChunk;
@ -193,8 +213,8 @@ public:
/// label = snapshot label /// label = snapshot label
/// ///
/// Returns: number of deleted snapshots /// Returns: number of deleted snapshots
long removeSnapshots(string label) { long removeSnapshots(string file) {
return _db.deleteSnapshot(label); return _db.deleteSnapshots(file);
} }
/// Delete a specific snapshot instance. /// Delete a specific snapshot instance.
@ -203,8 +223,8 @@ public:
/// snapshot = `Snapshot` to remove /// snapshot = `Snapshot` to remove
/// ///
/// Returns: `true` on success, `false` otherwise /// Returns: `true` on success, `false` otherwise
bool removeSnapshots(Snapshot snapshot) { bool removeSnapshot(Snapshot snapshot) {
return removeSnapshots(snapshot.id); return removeSnapshot(snapshot.id);
} }
/// Delete a snapshot by id. /// Delete a snapshot by id.
@ -213,7 +233,7 @@ public:
/// idSnapshot = snapshot id /// idSnapshot = snapshot id
/// ///
/// Returns: `true` if the row was deleted /// Returns: `true` if the row was deleted
bool removeSnapshots(long idSnapshot) { bool removeSnapshot(long idSnapshot) {
return _db.deleteSnapshot(idSnapshot) == idSnapshot; return _db.deleteSnapshot(idSnapshot) == idSnapshot;
} }
@ -233,10 +253,10 @@ public:
/// label = filter by exact label; empty string returns all /// label = filter by exact label; empty string returns all
/// ///
/// Returns: array of `Snapshot` handles /// Returns: array of `Snapshot` handles
Snapshot[] getSnapshots(string label = string.init) { Snapshot[] getSnapshots(string file = string.init) {
Snapshot[] snapshots; Snapshot[] snapshots;
foreach (snapshot; _db.getSnapshots(label)) { foreach (snapshot; _db.getSnapshots(file)) {
snapshots ~= new Snapshot(_db, snapshot); snapshots ~= new Snapshot(_db, snapshot);
} }

View file

@ -8,6 +8,18 @@ void main()
// Создаем временную базу для примера // Создаем временную базу для примера
string dbPath = "./bin/example.db"; string dbPath = "./bin/example.db";
if (exists(dbPath)) {
remove(dbPath);
}
Context context;
context.uid = 1000;
context.ruid = 1001;
context.uidName = "user1";
context.ruidName = "user2";
context.process = "mcedit";
// Инициализация Storage с компрессией Zstd // Инициализация Storage с компрессией Zstd
auto storage = new Storage(dbPath, true, Level.speed); auto storage = new Storage(dbPath, true, Level.speed);
@ -19,17 +31,17 @@ void main()
ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup; ubyte[] data2 = cast(ubyte[]) "Hello, updated cdcdb!".dup;
// Создание первого снимка // Создание первого снимка
auto snap1 = storage.newSnapshot("example_file", data1, "Версия 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.label); writeln("Файл: ", snap1.file);
writeln("Размер: ", snap1.length, " байт"); writeln("Размер: ", snap1.length, " байт");
writeln("Статус: ", snap1.status); writeln("Статус: ", snap1.status);
} }
// Создание второго снимка (обновление) // Создание второго снимка (обновление)
auto snap2 = storage.newSnapshot("example_file", data2, "Версия 2.0"); auto snap2 = storage.newSnapshot("example_file", data2, context, "Версия 2.0");
if (snap2) if (snap2)
{ {
writeln("Создан обновленный снимок с ID: ", snap2.id); writeln("Создан обновленный снимок с ID: ", snap2.id);