cdc/source/app.d
2025-09-04 22:04:29 +03:00

409 lines
11 KiB
D
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module app;
import std.stdio : writeln, writefln, File;
import std.file : exists, mkdirRecurse, read, write, readText, rename;
import std.path : baseName, dirName, buildPath, absolutePath;
import std.getopt : getopt;
import std.string : strip, split, splitLines;
import std.algorithm.searching : startsWith, endsWith;
import std.conv : to;
import std.datetime : Clock;
import std.exception : enforce;
import std.digest.sha : sha256Of, SHA256;
import std.format : format;
import fastcdc; // твой модуль FastCDC
// ---------- утилиты ----------
// У Phobos read(...) на некоторых версиях возвращает void[].
// Безопасно приводим к ubyte[] в @trusted-обёртке.
@trusted ubyte[] readBytes(string path)
{
auto v = read(path); // void[]
return cast(ubyte[]) v; // это новый буфер байт → безопасно
}
// hex из байтов (scope для локальных срезов)
@safe pure
string toHex(scope const(ubyte)[] bytes)
{
immutable char[16] HEX = "0123456789abcdef";
auto buf = new char[bytes.length * 2];
size_t j = 0;
foreach (b; bytes)
{
buf[j++] = HEX[(b >> 4) & 0xF];
buf[j++] = HEX[b & 0xF];
}
return buf.idup;
}
// Путь чанка с fanout: store/chunks/aa/bb/<hash>.bin
@safe
string chunkPath(string storeDir, string hashHex)
{
auto a = hashHex[0 .. 2];
auto b = hashHex[2 .. 4];
return buildPath(storeDir, "chunks", a, b, hashHex ~ ".bin");
}
// Путь манифеста: store/manifests/<name>.<epoch>.manifest
@safe
string manifestPath(string storeDir, string srcPath, long epoch)
{
auto name = baseName(srcPath);
return buildPath(storeDir, "manifests", name ~ "." ~ to!string(epoch) ~ ".manifest");
}
// Обновить/создать "указатель" на последний манифест:
// store/manifests/<name>.latest (содержит basename файла манифеста)
@safe
void writeLatestPointer(string storeDir, string srcPath, string manifestFullPath)
{
auto manifestsDir = buildPath(storeDir, "manifests");
mkdirRecurse(manifestsDir);
auto latestFile = buildPath(manifestsDir, baseName(srcPath) ~ ".latest");
auto justName = baseName(manifestFullPath);
write(latestFile, justName); // plain text
}
// Разрешить путь манифеста: если мне дали *.latest — прочитать ссылку.
@safe
string resolveManifestPath(string storeDir, string manifestGiven)
{
if (manifestGiven.endsWith(".latest") && exists(manifestGiven))
{
auto dir = dirName(manifestGiven);
auto name = readText(manifestGiven).strip;
// если в файле относительный basename — склеиваем с текущей директорией
return buildPath(dir, name);
}
// Возможно, дали просто store/manifests/<name>.latest (существующий файл)
if (manifestGiven.endsWith(".latest") && exists(buildPath(storeDir, "manifests", baseName(
manifestGiven))))
{
auto latest = buildPath(storeDir, "manifests", baseName(manifestGiven));
auto name = readText(latest).strip;
return buildPath(dirName(latest), name);
}
return manifestGiven;
}
// Атомарная запись чанка: через временный файл и rename()
@trusted
void writeAtomic(string path, in ubyte[] data)
{
auto tmp = path ~ ".tmp";
auto f = File(tmp, "wb");
f.rawWrite(data);
f.flush();
f.close();
// ensure конечные директории существуют (на случай гонки)
mkdirRecurse(dirName(path));
rename(tmp, path); // POSIX: атомарно
}
// Создать служебные директории
@safe
void ensureDirs(string storeDir)
{
mkdirRecurse(buildPath(storeDir, "chunks"));
mkdirRecurse(buildPath(storeDir, "manifests"));
}
// Вспомогалка: записать строку в файл И одновременно накормить хэшер
@trusted
void mfWriteLine(ref File mf, ref SHA256 h, string line)
{
mf.writeln(line);
h.put(cast(const(ubyte)[]) line);
h.put(cast(const(ubyte)[]) "\n");
}
// ---------- split ----------
struct SplitOpts
{
string storeDir;
string filePath;
size_t minSize = 8 * 1024;
size_t avgSize = 64 * 1024;
size_t maxSize = 256 * 1024;
size_t logEvery = 256; // каждые N чанков логировать (0 = выкл)
string profile; // "text" | "bin" | ""
}
@safe
void applyProfile(ref SplitOpts opt)
{
if (opt.profile == "text")
{
if (opt.minSize == 8 * 1024)
opt.minSize = 4 * 1024;
if (opt.avgSize == 64 * 1024)
opt.avgSize = 32 * 1024;
if (opt.maxSize == 256 * 1024)
opt.maxSize = 128 * 1024;
}
else if (opt.profile == "bin")
{
if (opt.minSize == 8 * 1024)
opt.minSize = 16 * 1024;
if (opt.avgSize == 64 * 1024)
opt.avgSize = 128 * 1024;
if (opt.maxSize == 256 * 1024)
opt.maxSize = 512 * 1024;
}
}
@safe
int cmdSplit(SplitOpts opt)
{
enforce(exists(opt.filePath), "Файл не найден: " ~ opt.filePath);
ensureDirs(opt.storeDir);
applyProfile(opt);
// бинарное чтение исходника
ubyte[] data = readBytes(opt.filePath);
FastCDCParams p = {opt.minSize, opt.avgSize, opt.maxSize};
p.normalize();
size_t chunkCount = 0;
size_t totalBytes = data.length;
// имя манифеста
auto epoch = Clock.currTime().toUnixTime();
auto mfPath = manifestPath(opt.storeDir, opt.filePath, epoch);
auto mfDir = buildPath(opt.storeDir, "manifests");
mkdirRecurse(mfDir);
auto mf = File(mfPath, "w");
// будем сразу считать SHA-256 манифеста (кроме финальной строки manifest_sha256)
SHA256 mh;
// шапка манифеста
mfWriteLine(mf, mh, "# FastCDC manifest");
mfWriteLine(mf, mh, "path\t" ~ absolutePath(opt.filePath));
mfWriteLine(mf, mh, format("size\t%s", to!string(totalBytes)));
mfWriteLine(mf, mh, "algo\tsha256");
mfWriteLine(mf, mh, format("min\t%u", cast(uint) p.minSize));
mfWriteLine(mf, mh, format("avg\t%u", cast(uint) p.avgSize));
mfWriteLine(mf, mh, format("max\t%u", cast(uint) p.maxSize));
mfWriteLine(mf, mh, "ord\thash\tsize");
// потоковая нарезка: sha256 чанка, атомарная запись, строка в манифест
size_t ord = 0;
processStream(data, p, (size_t start, size_t len) @safe {
auto slice = data[start .. start + len];
auto digest = sha256Of(slice); // ubyte[32]
auto hex = toHex(digest[]);
auto cpath = chunkPath(opt.storeDir, hex);
// подготовим подпапки aa/bb
mkdirRecurse(buildPath(opt.storeDir, "chunks", hex[0 .. 2]));
mkdirRecurse(buildPath(opt.storeDir, "chunks", hex[0 .. 2], hex[2 .. 4]));
if (!exists(cpath))
writeAtomic(cpath, slice); // атомарная запись
// строка манифеста для чанка
auto line = format("%u\t%s\t%u", cast(uint) ord, hex, cast(uint) len);
mfWriteLine(mf, mh, line);
++ord;
++chunkCount;
if (opt.logEvery != 0 && (ord % opt.logEvery) == 0)
writefln("… %u chunks", cast(uint) ord);
return 0; // продолжать
});
// финализируем хэш манифеста (без строки manifest_sha256) и добавляем контрольную строку
auto manifestDigest = mh.finish(); // ubyte[32]
auto manifestHex = toHex(manifestDigest[]);
mf.writeln("manifest_sha256\t" ~ manifestHex);
mf.flush();
mf.close();
// запишем указатель "последний манифест"
writeLatestPointer(opt.storeDir, opt.filePath, mfPath);
writefln("split: %s", opt.filePath);
writefln("store: %s", opt.storeDir);
writefln("manifest: %s", mfPath);
writefln("chunks: %u, bytes: %u", cast(uint) chunkCount, cast(uint) totalBytes);
return 0;
}
// ---------- restore ----------
struct RestoreOpts
{
string storeDir;
string manifestFile; // может быть *.latest
string outFile;
}
@safe
int cmdRestore(RestoreOpts opt)
{
auto realManifest = resolveManifestPath(opt.storeDir, opt.manifestFile);
enforce(exists(realManifest), "Манифест не найден: " ~ realManifest);
string text = readText(realManifest);
auto lines = splitLines(text);
// 1) Проверка целостности: пересчитать SHA-256 всех строк ДО manifest_sha256
SHA256 mh;
string expectedHex;
foreach (ln; lines)
{
auto s = ln.strip;
if (s.startsWith("manifest_sha256"))
{
auto cols = s.split('\t');
enforce(cols.length == 2, "Повреждённая строка manifest_sha256");
expectedHex = cols[1];
break;
}
// включаем в хэш строку и символ перевода строки
// (в split() выше переводы строк уже отрезаны)
mh.put(cast(const(ubyte)[]) ln);
mh.put(cast(const(ubyte)[]) "\n");
}
if (expectedHex.length)
{
auto gotHex = toHex(mh.finish()[]);
enforce(gotHex == expectedHex,
"Контрольная сумма манифеста не совпала:\n ожидалось: " ~ expectedHex ~ "\n получено: " ~ gotHex);
}
// 2) Найти секцию данных "ord\thash\tsize"
size_t i = 0;
while (i < lines.length && !lines[i].strip.startsWith("ord"))
++i;
enforce(i < lines.length, "Не найден заголовок секции данных в манифесте");
++i;
auto dst = File(opt.outFile, "wb");
size_t count = 0;
for (; i < lines.length; ++i)
{
auto ln = lines[i].strip;
if (ln.length == 0 || ln[0] == '#' || ln.startsWith("manifest_sha256"))
continue;
auto cols = ln.split('\t');
enforce(cols.length == 3, "Строка манифеста повреждена: " ~ ln);
auto hashHex = cols[1];
auto cpath = chunkPath(opt.storeDir, hashHex);
enforce(exists(cpath), "Чанк не найден: " ~ cpath);
ubyte[] chunkData = readBytes(cpath);
dst.rawWrite(chunkData);
++count;
}
dst.close();
writefln("restore: %s <- %s (chunks: %u)", opt.outFile, realManifest, cast(uint) count);
return 0;
}
// ---------- CLI ----------
@safe
void printHelp(string prog)
{
writeln("Usage:");
writeln(" ",
prog, " split --store <dir> <file> [--profile text|bin] [--min N] [--avg N] [--max N] [--log-every N]");
writeln(" ", prog, " restore --store <dir> <manifest|*.latest> <out_file>");
}
int main(string[] args)
{ // без @safe: getopt берёт &var
if (args.length < 2)
{
printHelp(args[0]);
return 1;
}
switch (args[1])
{
case "split":
{
SplitOpts opt;
string store;
string profile;
size_t minS = 0, avgS = 0, maxS = 0, logEvery = 256;
auto res = getopt(args,
"store", &store,
"profile", &profile,
"min", &minS,
"avg", &avgS,
"max", &maxS,
"log-every", &logEvery
);
if (res.helpWanted)
{
printHelp(args[0]);
return 0;
}
if (args.length < 3 || store.length == 0)
{
printHelp(args[0]);
return 1;
}
opt.storeDir = store;
opt.filePath = args[2];
opt.profile = profile;
if (minS)
opt.minSize = minS;
if (avgS)
opt.avgSize = avgS;
if (maxS)
opt.maxSize = maxS;
opt.logEvery = logEvery;
return cmdSplit(opt);
}
case "restore":
{
RestoreOpts opt;
string store;
auto res = getopt(args, "store", &store);
if (res.helpWanted)
{
printHelp(args[0]);
return 0;
}
if (args.length < 4 || store.length == 0)
{
printHelp(args[0]);
return 1;
}
opt.storeDir = store;
opt.manifestFile = args[2]; // можно передать *.latest
opt.outFile = args[3];
return cmdRestore(opt);
}
default:
printHelp(args[0]);
return 1;
}
}