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/.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/..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/.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/.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 [--profile text|bin] [--min N] [--avg N] [--max N] [--log-every N]"); writeln(" ", prog, " restore --store "); } 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; } }