Merge branch 'nightly' into 'unstable'

Nightly

See merge request termidesk/va/dwatch!11
This commit is contained in:
Alexander Zhirov 2025-11-14 21:48:27 +03:00
commit bc6ef2c62f
7 changed files with 368 additions and 27 deletions

View file

@ -3,15 +3,7 @@
"logfile": "/var/log/dwatch.log",
"watch": [
{
"path": "/home",
"rules": {
"tracking": [
"/alexander/Programming"
]
}
},
{
"path": "/etc"
"path": "/home/alexander/Programming/work/dwatch"
}
]
}

View file

@ -17,9 +17,10 @@
],
"copyright": "Copyright © 2025, Alexander Zhirov",
"dependencies": {
"archive": "~>0.7.1",
"cdcdb": {
"repository": "git+https://git.zhirov.kz/dwatch/cdcdb.git",
"version": "578e6a3358aa670543a220b73865f5f44e52a2a7"
"version": "3eaae341aab5797b210903b3ee5ad429c6b33fbe"
},
"commandr": "~>1.1.0",
"sdiff": {
@ -44,4 +45,4 @@
],
"targetPath": "bin",
"targetType": "executable"
}
}

View file

@ -1,8 +1,7 @@
{
"fileVersion": 1,
"versions": {
"arsd-official": "12.0.0",
"cdcdb": {"version":"578e6a3358aa670543a220b73865f5f44e52a2a7","repository":"git+https://git.zhirov.kz/dwatch/cdcdb.git"},
"archive": "0.7.1",
"commandr": "1.1.0",
"d2sqlite3": "1.0.0",
"sdiff": {"version":"~0.1.0","repository":"git+https://git.zhirov.kz/dlang/sdiff.git"},

View file

@ -35,20 +35,59 @@ int main(string[] args)
} else {
program.summary("CLI интерфейс для просмотра изменений в текстовых файлах")
.add(new Command("file", "Работа с файлами")
// .add(new Argument("filter", "Фильтр поиска файла")
// .optional
// )
.add(new Command("list", "Получить список файлов")
.add(new Flag("l", "long", "Печать длинного хеша")
.optional
)
.add(new Argument("name", "Фильтр по части имени файла")
.optional
)
)
.add(new Command("snapshots", "Получить список снимков")
.add(new Flag("l", "long", "Печать длинного хеша")
.optional
)
.add(new Argument("name", "Имя или хеш")
.required
)
)
.add(new Command("remove", "Удалить снимки")
.add(new Argument("name", "Имя или хеш")
.required
)
)
.add(new Command("export", "Выгрузить снимки")
.add(new Flag("l", "long", "Печать длинного хеша")
.optional
)
.add(new Argument("name", "Имя или хеш")
.required
)
.add(new Argument("path", "Путь для сохранения архива")
.required
)
)
)
.add(new Command("snapshot", "Работа со снимками")
.add(new Command("show", "Получить снимок")
.add(new Argument("snaphash", "Хеш снимка")
.required
)
)
.add(new Command("diff", "Получить изменения снимка")
.add(new Argument("snaphash1", "Хеш снимка")
.required
)
.add(new Argument("snaphash2", "Хеш относительного снимка")
.optional
)
)
.add(new Command("remove", "Удалить снимок")
.add(new Argument("snaphash", "Хеш снимка")
.required
)
)
);
// dwatch snapshot show <snaphash>
// dwatch snapshot diff <snaphash1> [<snaphash2>]
// dwatch snapshot remove <snaphash>
// dwatch file list <name-filter>
// dwatch file snapshots [-c|--count] <filename|filehash>
// dwatch file remove <filename|filehash>
// dwatch file export <filename|filehash>
}
ProgramArgs argumets = program.parse(args);
@ -74,6 +113,36 @@ int main(string[] args)
daemon.run();
} else {
DWCLI cli = new DWCLI(config);
scope(exit) cli.close();
argumets.on(
"file", (file) {
// dwatch file list [-l|--long] <name-filter>
file.on("list", fList => cli.fileList(
fList.flag("long"),
fList.arg("name")
))
// dwatch file snapshots [-c|--count] <filename|filehash>
.on("snapshots", fSnapshots => cli.fileSnapshots(
fSnapshots.flag("long"),
fSnapshots.arg("name")
))
// dwatch file remove <filename|filehash>
.on("remove", fRemove => cli.fileRemove(fRemove.arg("name")))
// dwatch file export <filename|filehash>
.on("export", fExport => cli.fileExport(
fExport.flag("long"),
fExport.arg("name"),
fExport.arg("path")
));
}
).on("snapshot", (snapshot) {
// dwatch snapshot show <snaphash>
snapshot.on("show", sSnapshot => cli.snapshotShow(sSnapshot.arg("snaphash")))
// dwatch snapshot diff <snaphash1> [<snaphash2>]
.on("diff", sDiff => cli.snapshotDiff(sDiff.arg("snaphash1"), sDiff.arg("snaphash2")))
// dwatch snapshot remove <snaphash>
.on("remove", sRemove => cli.snapshotRemove(sRemove.arg("snaphash")));
});
}
} catch (DWException e) {
e.print();

View file

@ -2,11 +2,289 @@ module dwatch.cli.core;
import dwatch.lib;
import cdcdb;
import sdiff;
import archive.targz : TarGzArchive;
import archive.tar : TarPermissions;
import std.stdio : writefln, writeln, write;
import std.ascii : isHexDigit;
import std.format : format;
import std.datetime.date : DateTime;
import std.conv : octal;
import std.file : fwrite = write, exists, mkdir;
import std.path : buildPath, baseName, extension, buildNormalizedPath, absolutePath;
import core.sys.posix.unistd : getuid;
final class DWCLI {
private:
Storage _storage;
bool isHexHash(const(char)[] s) @safe pure nothrow @nogc {
auto len = s.length;
if (len == 0 || len > 32) {
return false;
}
foreach (c; s) {
if (!isHexDigit(c)) return false;
}
return true;
}
Snapshot getSnapshot(string hash) {
if (!isHexHash(hash)) {
writeln("Хеш снимка некорректен");
return null;
}
Identifier id;
try {
id = Identifier(hash);
} catch (Exception e) {
writeln(e.msg);
return null;
}
Snapshot snapshot = _storage.getSnapshot(id);
if (snapshot is null) {
Snapshot[] snapshots = _storage.findSnapshot(id);
if (snapshots.length == 0) {
writeln("Снимок не найден");
return null;
}
if (snapshots.length > 1) {
writefln("Было найдено снимков: %s. Укажите полный хеш для точного совпадения.", snapshots.length);
return null;
}
snapshot = snapshots[0];
}
return snapshot;
}
StorageFile getFile(string nameHash) {
StorageFile storageFile =
isHexHash(nameHash) ? _storage.getFile(Identifier(nameHash)) : _storage.getFile(nameHash);
if (storageFile is null) {
StorageFile[] storageFiles =
isHexHash(nameHash) ? _storage.findFile(Identifier(nameHash)) : _storage.findFile(nameHash);
if (storageFiles.length == 0) {
writeln("Файл не найден");
return null;
}
if (storageFiles.length > 1) {
writefln("Было найдено файлов: %s", storageFiles.length);
foreach(size_t key, StorageFile sf; storageFiles) {
writefln(" %d.\t%s\t%s",
key + 1,
sf.id.toString,
sf.name
);
}
writeln("Укажите полный хеш или корректное имя для точного совпадения.");
return null;
}
storageFile = storageFiles[0];
}
return storageFile;
}
public:
this(const DWConfig config) {
import std.stdio : writeln;
writeln("CLI находится в разработке");
_storage = new Storage(config.database);
}
void close() {
_storage.close();
}
void fileList(bool longID, string name) {
StorageFile[] storageFiles;
if (name.length) {
storageFiles = _storage.findFile(name);
} else {
storageFiles = _storage.getFiles();
}
foreach (StorageFile file; storageFiles) {
Identifier id = file.id();
writefln("\t%s %s",
longID ? id.toString() : id.compact(),
file.name()
);
}
}
void fileSnapshots(bool longID, string nameHash) {
StorageFile storageFile = getFile(nameHash);
if (storageFile is null) return;
Snapshot[] snapshots = storageFile.snapshots();
if (snapshots.length == 0) {
writefln("%s %s - снимков не найдено",
longID ? storageFile.id.toString() : storageFile.id.compact(),
storageFile.name,
);
return;
}
writefln("%s %s - найдено снимков: %d",
longID ? storageFile.id.toString() : storageFile.id.compact(),
storageFile.name,
snapshots.length
);
foreach (size_t key, Snapshot snapshot; snapshots) {
auto current = snapshot.created;
auto dt = cast(DateTime) snapshot.created;
string date = format("%04d-%02d-%02d %02d:%02d:%02d",
dt.year, dt.month, dt.day,
current.hour, current.minute, current.second);
Identifier id = snapshot.id();
writefln(" %d.\t%s\t%s\t%s\t%s\t%s",
key + 1,
longID ? id.toString() : id.compact(),
date,
snapshot.process,
snapshot.uidName,
snapshot.ruidName
);
}
}
void fileRemove(string nameHash) {
if (getuid() != 0) {
writeln("Удаление файла требует права суперпользователя");
return;
}
StorageFile storageFile = getFile(nameHash);
if (storageFile is null) return;
_storage.deleteFile(storageFile.id) ?
writefln("Файл %s был успешно удален", storageFile.id.toString()) :
writefln("Файл %s не был удален", storageFile.id.toString());
}
void fileExport(bool longID, string nameHash, string path) {
if (!exists(path)) {
writeln("Путь для сохранения архива не существует");
return;
}
StorageFile storageFile = getFile(nameHash);
if (storageFile is null) return;
Snapshot[] snapshots = storageFile.snapshots();
string fileID = longID ? storageFile.id.toString() : storageFile.id.compact();
if (snapshots.length == 0) {
writefln("%s %s - снимков не найдено", fileID, storageFile.name);
return;
}
string filename = baseName(storageFile.name());
string tarGzArchive = buildPath(path.absolutePath.buildNormalizedPath, "%s-%s.tar.gz".format(fileID, filename));
TarGzArchive archive = new TarGzArchive();
foreach (size_t key, Snapshot snapshot; snapshots) {
auto current = snapshot.created;
auto dt = cast(DateTime) snapshot.created;
string date = format("%04d%02d%02d.%02d%02d%02d",
dt.year, dt.month, dt.day,
current.hour, current.minute, current.second);
string snapshotID = longID ? snapshot.id().toString() : snapshot.id().compact();
string snapshotName = "%s-%s-%s".format(date, snapshotID, filename);
TarGzArchive.File file = new TarGzArchive.File(snapshotName);
file.permissions = TarPermissions.FILE | octal!644;
file.data = snapshot.data();
archive.addFile(file);
writefln(" %d. Архивирование снимка: %s", key, snapshotName);
}
fwrite(tarGzArchive, cast(ubyte[]) archive.serialize());
writeln("Снимки выгружены в архив: ", tarGzArchive);
}
void snapshotShow(string hash) {
Snapshot snapshot = getSnapshot(hash);
if (snapshot is null) {
writeln("Не удалось получить снимок");
return;
}
string data = cast(string) snapshot.data();
data.write;
}
void snapshotDiff(string hash1, string hash2) {
Snapshot snapshot1 = getSnapshot(hash1);
Snapshot snapshot2;
if (snapshot1 is null) {
writeln("Не удалось получить основной снимок");
return;
}
if (hash2.length > 0) {
snapshot2 = getSnapshot(hash2);
if (snapshot2 is null) {
writeln("Не удалось получить относительный снимок");
return;
}
if (snapshot1.file != snapshot2.file) {
writeln("Относительный снимок не пренадлежит файлу основного снимка");
return;
}
} else {
// Поиск ближайшего относительного снимка
StorageFile storageFile = _storage.getFile(snapshot1.file());
Snapshot[] snapshots = storageFile.snapshots();
foreach (Snapshot snapshot; snapshots) {
if (snapshot.id == snapshot1.id) {
break;
}
snapshot2 = snapshot;
}
}
MMFile snapshotData1 = new MMFile(snapshot1.data());
MMFile snapshotData2;
if (snapshot2 is null) {
snapshotData2 = new MMFile("");
} else {
snapshotData2 = new MMFile(snapshot2.data());
}
snapshotData2.diff(snapshotData1).write;
}
void snapshotRemove(string hash) {
if (getuid() != 0) {
writeln("Удаление снимка требует права суперпользователя");
return;
}
Snapshot snapshot = getSnapshot(hash);
if (snapshot is null) {
writeln("Не удалось получить снимок");
return;
}
_storage.deleteSnapshot(snapshot.id) ?
writefln("Снимок %s был успешно удален", snapshot.id.toString()) :
writefln("Снимок %s не был удален", snapshot.id.toString());
}
}

View file

@ -52,6 +52,8 @@ private {
// Дочерний процесс записи снимков в базу данных
void worker(Tid mainTid, string database) {
auto storage = new Storage(database, true, 22);
scope(exit) storage.close();
storage.setupCDC(2048, 8192, 65_536, 0x7FFF, 0x7FF);
while (true) {

View file

@ -1,3 +1,3 @@
module dwatch.version_;
enum dwatchVersion = "0.0.8";
enum dwatchVersion = "0.0.9";