Переход на другую библиотеку

This commit is contained in:
Alexander Zhirov 2025-10-03 19:35:51 +03:00
parent 1f50b21457
commit 0fc56e7c04
Signed by: alexander
GPG key ID: C8D8BE544A27C511
5 changed files with 180 additions and 148 deletions

View file

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

View file

@ -2,6 +2,7 @@
"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"
} }
} }

View file

@ -1,6 +1,6 @@
module cdcdb.dblite; module cdcdb.dblite;
import arsd.sqlite : Sqlite, SqliteResult, DatabaseException; import d2sqlite3;
import std.string : join, replace, toLower; import std.string : join, replace, toLower;
import std.algorithm : canFind; import std.algorithm : canFind;
@ -91,19 +91,29 @@ struct DBSnapshotChunkData {
ubyte[32] zSha256; /// Хеш сжатого содержимого. ubyte[32] zSha256; /// Хеш сжатого содержимого.
} }
final class DBLite : Sqlite final class DBLite
{ {
private: private:
string _dbPath; /// Путь к файлу БД. string _dbPath; /// Путь к файлу БД.
size_t _maxRetries; /// Максимум повторов при `busy/locked`. size_t _maxRetries; /// Максимум повторов при `busy/locked`.
Database _db; /// Соединение с БД (d2sqlite3).
// SQL-схема (массив строковых запросов). // SQL-схема (массив строковых запросов).
mixin(import("scheme.d")); mixin(import("scheme.d"));
/// Выполняет SQL с повторными попытками при `locked/busy`. /// Выполняет SQL с повторными попытками при `locked/busy`.
SqliteResult sql(T...)(string queryText, T args) ResultRange sql(T...)(string queryText, T args)
{ {
// Готовим стейтмент сами, чтобы bindAll() работал и для BLOB.
auto attempt = () {
auto st = _db.prepare(queryText);
static if (T.length > 0)
st.bindAll(args);
return st.execute();
};
if (_maxRetries == 0) { if (_maxRetries == 0) {
return cast(SqliteResult) query(queryText, args); return attempt();
} }
string msg; string msg;
@ -111,10 +121,11 @@ private:
while (tryNo) { while (tryNo) {
try { try {
return cast(SqliteResult) query(queryText, args); return attempt();
} catch (DatabaseException e) { } catch (SqliteException e) {
msg = e.msg; msg = e.msg;
if (msg.toLower.canFind("locked", "busy")) { const code = e.code;
if (code == SQLITE_BUSY || code == SQLITE_LOCKED) {
if (--tryNo == 0) { if (--tryNo == 0) {
throw new Exception( throw new Exception(
"Не удалось выполнить запрос к базе данных после %d неудачных попыток: %s" "Не удалось выполнить запрос к базе данных после %d неудачных попыток: %s"
@ -123,17 +134,18 @@ private:
} }
continue; continue;
} }
break; break; // другие ошибки — дальше по стеку
} }
} }
throw new Exception(msg); // До сюда не дойдём, но для формальной полноты:
throw new Exception(msg.length ? msg : "SQLite error");
} }
/// Проверяет наличие обязательных таблиц. /// Проверяет наличие обязательных таблиц.
/// Если все отсутствуют — создаёт схему; если отсутствует часть — бросает ошибку. /// Если все отсутствуют — создаёт схему; если отсутствует часть — бросает ошибку.
void check() void check()
{ {
SqliteResult queryResult = sql( auto queryResult = sql(
q{ q{
WITH required(name) WITH required(name)
AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files")) AS (VALUES ("snapshots"), ("blobs"), ("snapshot_chunks"), ("users"), ("processes"), ("files"))
@ -151,7 +163,7 @@ private:
foreach (row; queryResult) foreach (row; queryResult)
{ {
missingTables ~= row["missing_table"].to!string; missingTables ~= row["missing_table"].as!string;
} }
enforce(missingTables.length == 0 || missingTables.length == 6, enforce(missingTables.length == 0 || missingTables.length == 6,
@ -171,16 +183,16 @@ public:
this(string database, size_t busyTimeout, size_t maxRetries) this(string database, size_t busyTimeout, size_t maxRetries)
{ {
_dbPath = database; _dbPath = database;
super(database); _db = Database(database);
check(); check();
_maxRetries = maxRetries; _maxRetries = maxRetries;
query("PRAGMA journal_mode=WAL"); _db.execute("PRAGMA journal_mode=WAL");
query("PRAGMA synchronous=NORMAL"); _db.execute("PRAGMA synchronous=NORMAL");
query("PRAGMA foreign_keys=ON"); _db.execute("PRAGMA foreign_keys=ON");
query("PRAGMA busy_timeout=%d".format(busyTimeout)); _db.execute("PRAGMA busy_timeout=%d".format(busyTimeout));
} }
/// BEGIN IMMEDIATE. /// BEGIN IMMEDIATE.
@ -192,13 +204,13 @@ public:
/// COMMIT. /// COMMIT.
void commit() void commit()
{ {
sql("COMMIT"); _db.commit();
} }
/// ROLLBACK. /// ROLLBACK.
void rollback() void rollback()
{ {
sql("ROLLBACK"); _db.rollback();
} }
/************************************************* /*************************************************
@ -218,8 +230,8 @@ public:
{ {
DBFile file; DBFile file;
file.id = cast(ubyte[]) row["id"].dup; file.id = row["id"].as!Blob(Blob.init);
file.path = row["name"].to!string; file.path = row["name"].as!string;
files ~= file; files ~= file;
} }
@ -236,10 +248,10 @@ public:
DBFile file; DBFile file;
if (!queryResult.empty()) { if (!queryResult.empty) {
auto data = queryResult.front(); auto data = queryResult.front;
file.id = cast(ubyte[]) data["id"].dup; file.id = data["id"].as!Blob(Blob.init);
file.path = path; file.path = path;
} }
@ -257,18 +269,18 @@ public:
DBFile file; DBFile file;
if (!queryResult.empty()) { if (!queryResult.empty) {
auto data = queryResult.front(); auto data = queryResult.front;
file.id = id; file.id = id;
file.path = data["name"].to!string; file.path = data["name"].as!string;
} }
return file; return file;
} }
DBFile addFile(string path) { DBFile addFile(string path) {
SqliteResult queryResult; ResultRange queryResult;
UUID uuid; UUID uuid;
// Исключение одинакового UUID первичного ключа // Исключение одинакового UUID первичного ключа
@ -279,7 +291,7 @@ public:
SELECT id FROM files WHERE id = ?1 SELECT id FROM files WHERE id = ?1
}, uuid.data[] }, uuid.data[]
); );
} while (!queryResult.empty()); } while (!queryResult.empty);
queryResult = sql( queryResult = sql(
q{ q{
@ -289,7 +301,7 @@ public:
}, uuid.data[], path }, uuid.data[], path
); );
enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); enforce(!queryResult.empty, "Не удалось добавить новый файл в базу данных");
return DBFile(Identifier(uuid.data), path); return DBFile(Identifier(uuid.data), path);
} }
@ -307,8 +319,8 @@ public:
{ {
DBFile file; DBFile file;
file.id = cast(ubyte[]) row["id"].dup; file.id = row["id"].as!Blob(Blob.init);
file.path = row["name"].to!string; file.path = row["name"].as!string;
files ~= file; files ~= file;
} }
@ -339,8 +351,8 @@ public:
{ {
DBFile file; DBFile file;
file.id = cast(ubyte[]) row["id"].dup; file.id = row["id"].as!Blob(Blob.init);
file.path = row["name"].to!string; file.path = row["name"].as!string;
files ~= file; files ~= file;
} }
@ -350,12 +362,12 @@ public:
bool deleteFile(Identifier id) { bool deleteFile(Identifier id) {
auto queryResult = sql("DELETE FROM files WHERE id = ?1 RETURNING id", id[]); auto queryResult = sql("DELETE FROM files WHERE id = ?1 RETURNING id", id[]);
return !queryResult.empty(); return !queryResult.empty;
} }
bool deleteFile(string path) { bool deleteFile(string path) {
auto queryResult = sql("DELETE FROM files WHERE name = ?1 RETURNING id", path); auto queryResult = sql("DELETE FROM files WHERE name = ?1 RETURNING id", path);
return !queryResult.empty(); return !queryResult.empty;
} }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
@ -376,8 +388,8 @@ public:
}, id[], sha256[] }, id[], sha256[]
); );
if (!queryResult.empty()) if (!queryResult.empty)
return queryResult.front()["is_last"].to!long > 0; return queryResult.front["is_last"].as!long > 0;
return false; return false;
} }
@ -393,7 +405,7 @@ public:
}, uid, name }, uid, name
); );
return !queryResult.empty(); return !queryResult.empty;
} }
DBProcess getProcess(string name) { DBProcess getProcess(string name) {
@ -405,10 +417,10 @@ public:
DBProcess process; DBProcess process;
if (!queryResult.empty()) { if (!queryResult.empty) {
auto data = queryResult.front(); auto data = queryResult.front;
process.id = cast(ubyte[]) data["id"].dup; process.id = data["id"].as!Blob(Blob.init);
process.name = name; process.name = name;
} }
@ -426,18 +438,18 @@ public:
DBProcess process; DBProcess process;
if (!queryResult.empty()) { if (!queryResult.empty) {
auto data = queryResult.front(); auto data = queryResult.front;
process.id = id; process.id = id;
process.name = data["name"].to!string; process.name = data["name"].as!string;
} }
return process; return process;
} }
DBProcess addProcess(string name) { DBProcess addProcess(string name) {
SqliteResult queryResult; ResultRange queryResult;
UUID uuid; UUID uuid;
// Исключение одинакового UUID первичного ключа // Исключение одинакового UUID первичного ключа
@ -448,7 +460,7 @@ public:
SELECT id FROM processes WHERE id = ?1 SELECT id FROM processes WHERE id = ?1
}, uuid.data[] }, uuid.data[]
); );
} while (!queryResult.empty()); } while (!queryResult.empty);
queryResult = sql( queryResult = sql(
q{ q{
@ -458,7 +470,7 @@ public:
}, uuid.data[], name }, uuid.data[], name
); );
enforce(!queryResult.empty(), "Не удалось добавить новый файл в базу данных"); enforce(!queryResult.empty, "Не удалось добавить новый файл в базу данных");
return DBProcess(Identifier(uuid.data), name); return DBProcess(Identifier(uuid.data), name);
} }
@ -467,7 +479,7 @@ public:
bool addSnapshot(ref DBSnapshot snapshot) bool addSnapshot(ref DBSnapshot snapshot)
{ {
SqliteResult queryResult; ResultRange queryResult;
UUID uuid; UUID uuid;
// Исключение одинакового UUID первичного ключа // Исключение одинакового UUID первичного ключа
@ -478,7 +490,7 @@ public:
SELECT id FROM snapshots WHERE id = ?1 SELECT id FROM snapshots WHERE id = ?1
}, uuid.data[] }, uuid.data[]
); );
} while (!queryResult.empty()); } while (!queryResult.empty);
import std.datetime : Clock; import std.datetime : Clock;
@ -524,7 +536,7 @@ public:
snapshot.status.to!int // ?15 snapshot.status.to!int // ?15
); );
return !queryResult.empty(); return !queryResult.empty;
} }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
@ -564,7 +576,7 @@ public:
blob.zstd.to!int // ?8 blob.zstd.to!int // ?8
); );
return !queryResult.empty(); return !queryResult.empty;
} }
bool addSnapshotChunk(DBSnapshotChunk snapshotChunk) bool addSnapshotChunk(DBSnapshotChunk snapshotChunk)
@ -581,7 +593,7 @@ public:
snapshotChunk.sha256[] snapshotChunk.sha256[]
); );
return !queryResult.empty(); return !queryResult.empty;
} }
DBSnapshotChunkData[] getChunks(Identifier id) DBSnapshotChunkData[] getChunks(Identifier id)
@ -603,14 +615,22 @@ public:
{ {
DBSnapshotChunkData sdch; DBSnapshotChunkData sdch;
sdch.chunkIndex = row["chunk_index"].to!long; sdch.chunkIndex = row["chunk_index"].as!long;
sdch.offset = row["offset"].to!long; sdch.offset = row["offset"].as!long;
sdch.size = row["size"].to!long; sdch.size = row["size"].as!long;
sdch.content = cast(ubyte[]) row["content"].dup;
sdch.zstd = cast(bool) row["zstd"].to!int; // content может быть NULL
sdch.zSize = row["z_size"].to!long; auto contentBlob = cast(ubyte[]) row["content"].as!Blob(Blob.init);
sdch.sha256 = cast(ubyte[]) row["sha256"].dup; sdch.content = contentBlob.length ? contentBlob.dup : null;
sdch.zSha256 = cast(ubyte[]) row["z_sha256"].dup;
sdch.zstd = row["zstd"].as!int != 0;
sdch.zSize = row["z_size"].as!long;
auto sha = cast(ubyte[]) row["sha256"].as!Blob(Blob.init);
if (sha.length) sdch.sha256[] = sha;
auto zsha = cast(ubyte[]) row["z_sha256"].as!Blob(Blob.init);
if (zsha.length) sdch.zSha256[] = zsha;
sdchs ~= sdch; sdchs ~= sdch;
} }
@ -657,28 +677,28 @@ public:
{ {
DBSnapshot snapshot; DBSnapshot snapshot;
snapshot.id = cast(ubyte[]) row["id"].dup; snapshot.id = row["id"].as!Blob(Blob.init);
snapshot.file = DBFile( snapshot.file = DBFile(
Identifier(cast(ubyte[]) row["file_id"].dup), Identifier(row["file_id"].as!Blob(Blob.init)),
row["file_name"].to!string row["file_name"].as!string
); );
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; snapshot.sha256 = row["sha256"].as!Blob(Blob.init);
snapshot.description = row["description"].to!string; snapshot.description = row["description"].as!string(""); // может быть NULL
snapshot.createdUtc = row["created_utc"].to!long; snapshot.createdUtc = row["created_utc"].as!long;
snapshot.sourceLength = row["source_length"].to!long; snapshot.sourceLength = row["source_length"].as!long;
snapshot.algoMin = row["algo_min"].to!long; snapshot.algoMin = row["algo_min"].as!long;
snapshot.algoNormal = row["algo_normal"].to!long; snapshot.algoNormal = row["algo_normal"].as!long;
snapshot.algoMax = row["algo_max"].to!long; snapshot.algoMax = row["algo_max"].as!long;
snapshot.maskS = row["mask_s"].to!long; snapshot.maskS = row["mask_s"].as!long;
snapshot.maskL = row["mask_l"].to!long; snapshot.maskL = row["mask_l"].as!long;
snapshot.status = cast(SnapshotStatus) row["status"].to!int; snapshot.status = cast(SnapshotStatus) row["status"].as!int;
snapshot.uid = row["uid"].to!long; snapshot.uid = row["uid"].as!long;
snapshot.ruid = row["ruid"].to!long; snapshot.ruid = row["ruid"].as!long;
snapshot.uidName = row["uid_name"].to!string; snapshot.uidName = row["uid_name"].as!string;
snapshot.ruidName = row["ruid_name"].to!string; snapshot.ruidName = row["ruid_name"].as!string;
snapshot.process = DBProcess( snapshot.process = DBProcess(
Identifier(cast(ubyte[]) row["process_id"].dup), Identifier(row["process_id"].as!Blob(Blob.init)),
row["process_name"].to!string row["process_name"].as!string
); );
snapshots ~= snapshot; snapshots ~= snapshot;
@ -727,30 +747,29 @@ public:
{ {
DBSnapshot snapshot; DBSnapshot snapshot;
snapshot.id = cast(ubyte[]) row["id"].dup; snapshot.id = row["id"].as!Blob(Blob.init);
snapshot.file = DBFile( snapshot.file = DBFile(
Identifier(cast(ubyte[]) row["file_id"].dup), Identifier(row["file_id"].as!Blob(Blob.init)),
row["file_name"].to!string row["file_name"].as!string
); );
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; snapshot.sha256 = row["sha256"].as!Blob(Blob.init);
snapshot.description = row["description"].to!string; snapshot.description = row["description"].as!string("");
snapshot.createdUtc = row["created_utc"].to!long; snapshot.createdUtc = row["created_utc"].as!long;
snapshot.sourceLength = row["source_length"].to!long; snapshot.sourceLength = row["source_length"].as!long;
snapshot.algoMin = row["algo_min"].to!long; snapshot.algoMin = row["algo_min"].as!long;
snapshot.algoNormal = row["algo_normal"].to!long; snapshot.algoNormal = row["algo_normal"].as!long;
snapshot.algoMax = row["algo_max"].to!long; snapshot.algoMax = row["algo_max"].as!long;
snapshot.maskS = row["mask_s"].to!long; snapshot.maskS = row["mask_s"].as!long;
snapshot.maskL = row["mask_l"].to!long; snapshot.maskL = row["mask_l"].as!long;
snapshot.status = cast(SnapshotStatus) row["status"].to!int; snapshot.status = cast(SnapshotStatus) row["status"].as!int;
snapshot.uid = row["uid"].to!long; snapshot.uid = row["uid"].as!long;
snapshot.ruid = row["ruid"].to!long; snapshot.ruid = row["ruid"].as!long;
snapshot.uidName = row["uid_name"].to!string; snapshot.uidName = row["uid_name"].as!string;
snapshot.ruidName = row["ruid_name"].to!string; snapshot.ruidName = row["ruid_name"].as!string;
snapshot.process = DBProcess( snapshot.process = DBProcess(
Identifier(cast(ubyte[]) row["process_id"].dup), Identifier(row["process_id"].as!Blob(Blob.init)),
row["process_name"].to!string row["process_name"].as!string
); );
snapshots ~= snapshot; snapshots ~= snapshot;
} }
@ -793,31 +812,31 @@ public:
DBSnapshot snapshot; DBSnapshot snapshot;
if (!queryResult.empty()) { if (!queryResult.empty) {
auto data = queryResult.front(); auto data = queryResult.front;
snapshot.id = cast(ubyte[]) data["id"].dup; snapshot.id = data["id"].as!Blob(Blob.init);
snapshot.file = DBFile( snapshot.file = DBFile(
Identifier(cast(ubyte[]) data["file_id"].dup), Identifier(data["file_id"].as!Blob(Blob.init)),
data["file_name"].to!string data["file_name"].as!string
); );
snapshot.sha256 = cast(ubyte[]) data["sha256"].dup; snapshot.sha256 = data["sha256"].as!Blob(Blob.init);
snapshot.description = data["description"].to!string; snapshot.description = data["description"].as!string("");
snapshot.createdUtc = data["created_utc"].to!long; snapshot.createdUtc = data["created_utc"].as!long;
snapshot.sourceLength = data["source_length"].to!long; snapshot.sourceLength = data["source_length"].as!long;
snapshot.algoMin = data["algo_min"].to!long; snapshot.algoMin = data["algo_min"].as!long;
snapshot.algoNormal = data["algo_normal"].to!long; snapshot.algoNormal = data["algo_normal"].as!long;
snapshot.algoMax = data["algo_max"].to!long; snapshot.algoMax = data["algo_max"].as!long;
snapshot.maskS = data["mask_s"].to!long; snapshot.maskS = data["mask_s"].as!long;
snapshot.maskL = data["mask_l"].to!long; snapshot.maskL = data["mask_l"].as!long;
snapshot.status = cast(SnapshotStatus) data["status"].to!int; snapshot.status = cast(SnapshotStatus) data["status"].as!int;
snapshot.uid = data["uid"].to!long; snapshot.uid = data["uid"].as!long;
snapshot.ruid = data["ruid"].to!long; snapshot.ruid = data["ruid"].as!long;
snapshot.uidName = data["uid_name"].to!string; snapshot.uidName = data["uid_name"].as!string;
snapshot.ruidName = data["ruid_name"].to!string; snapshot.ruidName = data["ruid_name"].as!string;
snapshot.process = DBProcess( snapshot.process = DBProcess(
Identifier(cast(ubyte[]) data["process_id"].dup), Identifier(data["process_id"].as!Blob(Blob.init)),
data["process_name"].to!string data["process_name"].as!string
); );
} }
@ -870,28 +889,28 @@ public:
{ {
DBSnapshot snapshot; DBSnapshot snapshot;
snapshot.id = cast(ubyte[]) row["id"].dup; snapshot.id = row["id"].as!Blob(Blob.init);
snapshot.file = DBFile( snapshot.file = DBFile(
Identifier(cast(ubyte[]) row["file_id"].dup), Identifier(row["file_id"].as!Blob(Blob.init)),
row["file_name"].to!string row["file_name"].as!string
); );
snapshot.sha256 = cast(ubyte[]) row["sha256"].dup; snapshot.sha256 = row["sha256"].as!Blob(Blob.init);
snapshot.description = row["description"].to!string; snapshot.description = row["description"].as!string("");
snapshot.createdUtc = row["created_utc"].to!long; snapshot.createdUtc = row["created_utc"].as!long;
snapshot.sourceLength = row["source_length"].to!long; snapshot.sourceLength = row["source_length"].as!long;
snapshot.algoMin = row["algo_min"].to!long; snapshot.algoMin = row["algo_min"].as!long;
snapshot.algoNormal = row["algo_normal"].to!long; snapshot.algoNormal = row["algo_normal"].as!long;
snapshot.algoMax = row["algo_max"].to!long; snapshot.algoMax = row["algo_max"].as!long;
snapshot.maskS = row["mask_s"].to!long; snapshot.maskS = row["mask_s"].as!long;
snapshot.maskL = row["mask_l"].to!long; snapshot.maskL = row["mask_l"].as!long;
snapshot.status = cast(SnapshotStatus) row["status"].to!int; snapshot.status = cast(SnapshotStatus) row["status"].as!int;
snapshot.uid = row["uid"].to!long; snapshot.uid = row["uid"].as!long;
snapshot.ruid = row["ruid"].to!long; snapshot.ruid = row["ruid"].as!long;
snapshot.uidName = row["uid_name"].to!string; snapshot.uidName = row["uid_name"].as!string;
snapshot.ruidName = row["ruid_name"].to!string; snapshot.ruidName = row["ruid_name"].as!string;
snapshot.process = DBProcess( snapshot.process = DBProcess(
Identifier(cast(ubyte[]) row["process_id"].dup), Identifier(row["process_id"].as!Blob(Blob.init)),
row["process_name"].to!string row["process_name"].as!string
); );
snapshots ~= snapshot; snapshots ~= snapshot;
@ -902,6 +921,6 @@ public:
bool deleteSnapshot(Identifier id) { bool deleteSnapshot(Identifier id) {
auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id[]); auto queryResult = sql("DELETE FROM snapshots WHERE id = ? RETURNING id", id[]);
return !queryResult.empty(); return !queryResult.empty;
} }
} }

View file

@ -60,12 +60,24 @@ public:
_data = data; _data = data;
} }
this(immutable(ubyte[]) data)
{
assert(data.length <= 16);
_data = data.dup;
}
this(ref const ubyte[16] data) this(ref const ubyte[16] data)
{ {
assert(data.length <= 16); assert(data.length <= 16);
_data = data.dup; _data = data.dup;
} }
void opAssign(immutable(ubyte[]) data)
{
assert(data.length <= 16);
_data = data.dup;
}
void opAssign(ubyte[] data) void opAssign(ubyte[] data)
{ {
assert(data.length <= 16); assert(data.length <= 16);

View file

@ -72,13 +72,13 @@ void main()
} }
// Удаление файла // Удаление файла
if (storage.deleteFile("example_file")) // if (storage.deleteFile("example_file"))
writeln("Файл example_file удален."); // writeln("Файл example_file удален.");
// Проверка: снимки удалены // // Проверка: снимки удалены
auto remaining = storage.getSnapshots("example_file"); // auto remaining = storage.getSnapshots("example_file");
assert(remaining.length == 0); // assert(remaining.length == 0);
writeln("Все снимки удалены."); // writeln("Все снимки удалены.");
writeln("Версия библиотеки: ", storage.getVersion()); // writeln("Версия библиотеки: ", storage.getVersion());
} }