forked from dlang/cdcdb
Обновлена схема БД - добавлены таблицы users, processes, files. Добавлен параметр Context с идентификацией пользователей и процесса. label сменен на file.
This commit is contained in:
parent
5bb4d65c92
commit
49ee7a4053
7 changed files with 289 additions and 168 deletions
|
@ -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`).
|
|
98
README.ru.md
98
README.ru.md
|
@ -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).
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
test/app.d
18
test/app.d
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue