mod
This commit is contained in:
		
							parent
							
								
									f1dd2aebb2
								
							
						
					
					
						commit
						90a7f3b548
					
				
					 1 changed files with 183 additions and 46 deletions
				
			
		
							
								
								
									
										229
									
								
								source/app.d
									
										
									
									
									
								
							
							
						
						
									
										229
									
								
								source/app.d
									
										
									
									
									
								
							| 
						 | 
					@ -1,21 +1,30 @@
 | 
				
			||||||
module app;
 | 
					module app;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import std.stdio : writeln, writefln, File;
 | 
					import std.stdio : writeln, writefln, File;
 | 
				
			||||||
import std.file : exists, mkdirRecurse, read, write, readText;
 | 
					import std.file : exists, mkdirRecurse, read, write, readText, rename;
 | 
				
			||||||
import std.path : baseName, buildPath, absolutePath;
 | 
					import std.path : baseName, dirName, buildPath, absolutePath;
 | 
				
			||||||
import std.getopt : getopt;
 | 
					import std.getopt : getopt;
 | 
				
			||||||
import std.string : strip, split, splitLines;
 | 
					import std.string : strip, split, splitLines;
 | 
				
			||||||
import std.algorithm.searching : startsWith;
 | 
					import std.algorithm.searching : startsWith, endsWith;
 | 
				
			||||||
import std.conv : to;
 | 
					import std.conv : to;
 | 
				
			||||||
import std.datetime : Clock;
 | 
					import std.datetime : Clock;
 | 
				
			||||||
import std.exception : enforce;
 | 
					import std.exception : enforce;
 | 
				
			||||||
import std.digest.sha : sha256Of;
 | 
					import std.digest.sha : sha256Of, SHA256;
 | 
				
			||||||
 | 
					import std.format : format;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import fastcdc; // твой модуль FastCDC
 | 
					import fastcdc; // твой модуль FastCDC
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ---------- утилиты ----------
 | 
					// ---------- утилиты ----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// hex: параметр scope, чтобы можно было безопасно передавать срез локального массива
 | 
					// У Phobos read(...) на некоторых версиях возвращает void[].
 | 
				
			||||||
 | 
					// Безопасно приводим к ubyte[] в @trusted-обёртке.
 | 
				
			||||||
 | 
					@trusted ubyte[] readBytes(string path)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						auto v = read(path); // void[]
 | 
				
			||||||
 | 
						return cast(ubyte[]) v; // это новый буфер байт → безопасно
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// hex из байтов (scope для локальных срезов)
 | 
				
			||||||
@safe pure
 | 
					@safe pure
 | 
				
			||||||
string toHex(scope const(ubyte)[] bytes)
 | 
					string toHex(scope const(ubyte)[] bytes)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -27,10 +36,10 @@ string toHex(scope const(ubyte)[] bytes)
 | 
				
			||||||
		buf[j++] = HEX[(b >> 4) & 0xF];
 | 
							buf[j++] = HEX[(b >> 4) & 0xF];
 | 
				
			||||||
		buf[j++] = HEX[b & 0xF];
 | 
							buf[j++] = HEX[b & 0xF];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return buf.idup; // immutable string
 | 
						return buf.idup;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// fanout: store/chunks/aa/bb/<hash>.bin
 | 
					// Путь чанка с fanout: store/chunks/aa/bb/<hash>.bin
 | 
				
			||||||
@safe
 | 
					@safe
 | 
				
			||||||
string chunkPath(string storeDir, string hashHex)
 | 
					string chunkPath(string storeDir, string hashHex)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -39,7 +48,7 @@ string chunkPath(string storeDir, string hashHex)
 | 
				
			||||||
	return buildPath(storeDir, "chunks", a, b, hashHex ~ ".bin");
 | 
						return buildPath(storeDir, "chunks", a, b, hashHex ~ ".bin");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// manifest: store/manifests/<name>.<epoch>.manifest
 | 
					// Путь манифеста: store/manifests/<name>.<epoch>.manifest
 | 
				
			||||||
@safe
 | 
					@safe
 | 
				
			||||||
string manifestPath(string storeDir, string srcPath, long epoch)
 | 
					string manifestPath(string storeDir, string srcPath, long epoch)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -47,6 +56,55 @@ string manifestPath(string storeDir, string srcPath, long epoch)
 | 
				
			||||||
	return buildPath(storeDir, "manifests", name ~ "." ~ to!string(epoch) ~ ".manifest");
 | 
						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
 | 
					@safe
 | 
				
			||||||
void ensureDirs(string storeDir)
 | 
					void ensureDirs(string storeDir)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -54,11 +112,13 @@ void ensureDirs(string storeDir)
 | 
				
			||||||
	mkdirRecurse(buildPath(storeDir, "manifests"));
 | 
						mkdirRecurse(buildPath(storeDir, "manifests"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@trusted ubyte[] readBytes(string path)
 | 
					// Вспомогалка: записать строку в файл И одновременно накормить хэшер
 | 
				
			||||||
 | 
					@trusted
 | 
				
			||||||
 | 
					void mfWriteLine(ref File mf, ref SHA256 h, string line)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	// std.file.read выделяет новый буфер байтов → безопасно привести к ubyte[]
 | 
						mf.writeln(line);
 | 
				
			||||||
	auto v = read(path); // void[]
 | 
						h.put(cast(const(ubyte)[]) line);
 | 
				
			||||||
	return cast(ubyte[]) v; // доверяем Phobos: это сырой байтовый буфер
 | 
						h.put(cast(const(ubyte)[]) "\n");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ---------- split ----------
 | 
					// ---------- split ----------
 | 
				
			||||||
| 
						 | 
					@ -70,6 +130,31 @@ struct SplitOpts
 | 
				
			||||||
	size_t minSize = 8 * 1024;
 | 
						size_t minSize = 8 * 1024;
 | 
				
			||||||
	size_t avgSize = 64 * 1024;
 | 
						size_t avgSize = 64 * 1024;
 | 
				
			||||||
	size_t maxSize = 256 * 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
 | 
					@safe
 | 
				
			||||||
| 
						 | 
					@ -77,8 +162,9 @@ int cmdSplit(SplitOpts opt)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	enforce(exists(opt.filePath), "Файл не найден: " ~ opt.filePath);
 | 
						enforce(exists(opt.filePath), "Файл не найден: " ~ opt.filePath);
 | 
				
			||||||
	ensureDirs(opt.storeDir);
 | 
						ensureDirs(opt.storeDir);
 | 
				
			||||||
 | 
						applyProfile(opt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// бинарное чтение: std.file.read возвращает ubyte[]
 | 
						// бинарное чтение исходника
 | 
				
			||||||
	ubyte[] data = readBytes(opt.filePath);
 | 
						ubyte[] data = readBytes(opt.filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	FastCDCParams p = {opt.minSize, opt.avgSize, opt.maxSize};
 | 
						FastCDCParams p = {opt.minSize, opt.avgSize, opt.maxSize};
 | 
				
			||||||
| 
						 | 
					@ -87,48 +173,69 @@ int cmdSplit(SplitOpts opt)
 | 
				
			||||||
	size_t chunkCount = 0;
 | 
						size_t chunkCount = 0;
 | 
				
			||||||
	size_t totalBytes = data.length;
 | 
						size_t totalBytes = data.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// имя манифеста
 | 
				
			||||||
	auto epoch = Clock.currTime().toUnixTime();
 | 
						auto epoch = Clock.currTime().toUnixTime();
 | 
				
			||||||
	auto mfPath = manifestPath(opt.storeDir, opt.filePath, epoch);
 | 
						auto mfPath = manifestPath(opt.storeDir, opt.filePath, epoch);
 | 
				
			||||||
	mkdirRecurse(buildPath(opt.storeDir, "manifests"));
 | 
						auto mfDir = buildPath(opt.storeDir, "manifests");
 | 
				
			||||||
 | 
						mkdirRecurse(mfDir);
 | 
				
			||||||
	auto mf = File(mfPath, "w");
 | 
						auto mf = File(mfPath, "w");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// шапка манифеста
 | 
						// будем сразу считать SHA-256 манифеста (кроме финальной строки manifest_sha256)
 | 
				
			||||||
	mf.writeln("# FastCDC manifest");
 | 
						SHA256 mh;
 | 
				
			||||||
	mf.writefln("path\t%s", absolutePath(opt.filePath));
 | 
					 | 
				
			||||||
	mf.writefln("size\t%s", to!string(totalBytes));
 | 
					 | 
				
			||||||
	mf.writefln("algo\tsha256");
 | 
					 | 
				
			||||||
	mf.writefln("min\t%u", cast(uint) p.minSize);
 | 
					 | 
				
			||||||
	mf.writefln("avg\t%u", cast(uint) p.avgSize);
 | 
					 | 
				
			||||||
	mf.writefln("max\t%u", cast(uint) p.maxSize);
 | 
					 | 
				
			||||||
	mf.writeln("ord\thash\tsize");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// шапка манифеста
 | 
				
			||||||
 | 
						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;
 | 
						size_t ord = 0;
 | 
				
			||||||
	processStream(data, p, (size_t start, size_t len) @safe {
 | 
						processStream(data, p, (size_t start, size_t len) @safe {
 | 
				
			||||||
		auto slice = data[start .. start + len];
 | 
							auto slice = data[start .. start + len];
 | 
				
			||||||
		auto digest = sha256Of(slice); // ubyte[32] (на стеке)
 | 
							auto digest = sha256Of(slice); // ubyte[32]
 | 
				
			||||||
		auto hex = toHex(digest[]); // scope-параметр — ок
 | 
							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]));
 | 
				
			||||||
		mkdirRecurse(buildPath(opt.storeDir, "chunks", hex[0 .. 2], hex[2 .. 4]));
 | 
							mkdirRecurse(buildPath(opt.storeDir, "chunks", hex[0 .. 2], hex[2 .. 4]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		auto cpath = chunkPath(opt.storeDir, hex);
 | 
					 | 
				
			||||||
		if (!exists(cpath))
 | 
							if (!exists(cpath))
 | 
				
			||||||
			write(cpath, slice);
 | 
								writeAtomic(cpath, slice); // атомарная запись
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// строка манифеста для чанка
 | 
				
			||||||
 | 
							auto line = format("%u\t%s\t%u", cast(uint) ord, hex, cast(uint) len);
 | 
				
			||||||
 | 
							mfWriteLine(mf, mh, line);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		mf.writefln("%u\t%s\t%u", cast(uint) ord, hex, cast(uint) len);
 | 
					 | 
				
			||||||
		++ord;
 | 
							++ord;
 | 
				
			||||||
		++chunkCount;
 | 
							++chunkCount;
 | 
				
			||||||
		return 0;
 | 
							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.flush();
 | 
				
			||||||
	mf.close();
 | 
						mf.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// запишем указатель "последний манифест"
 | 
				
			||||||
 | 
						writeLatestPointer(opt.storeDir, opt.filePath, mfPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	writefln("split: %s", opt.filePath);
 | 
						writefln("split: %s", opt.filePath);
 | 
				
			||||||
	writefln("store: %s", opt.storeDir);
 | 
						writefln("store: %s", opt.storeDir);
 | 
				
			||||||
	writefln("manifest: %s", mfPath);
 | 
						writefln("manifest: %s", mfPath);
 | 
				
			||||||
	writefln("chunks: %u, bytes: %u",
 | 
						writefln("chunks: %u, bytes: %u", cast(uint) chunkCount, cast(uint) totalBytes);
 | 
				
			||||||
		cast(uint) chunkCount, cast(uint) totalBytes);
 | 
					 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,19 +244,45 @@ int cmdSplit(SplitOpts opt)
 | 
				
			||||||
struct RestoreOpts
 | 
					struct RestoreOpts
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	string storeDir;
 | 
						string storeDir;
 | 
				
			||||||
	string manifestFile;
 | 
						string manifestFile; // может быть *.latest
 | 
				
			||||||
	string outFile;
 | 
						string outFile;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@safe
 | 
					@safe
 | 
				
			||||||
int cmdRestore(RestoreOpts opt)
 | 
					int cmdRestore(RestoreOpts opt)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	enforce(exists(opt.manifestFile), "Манифест не найден: " ~ opt.manifestFile);
 | 
						auto realManifest = resolveManifestPath(opt.storeDir, opt.manifestFile);
 | 
				
			||||||
 | 
						enforce(exists(realManifest), "Манифест не найден: " ~ realManifest);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	string text = readText(opt.manifestFile);
 | 
						string text = readText(realManifest);
 | 
				
			||||||
	auto lines = splitLines(text);
 | 
						auto lines = splitLines(text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// найти строку "ord\thash\tsize"
 | 
						// 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;
 | 
						size_t i = 0;
 | 
				
			||||||
	while (i < lines.length && !lines[i].strip.startsWith("ord"))
 | 
						while (i < lines.length && !lines[i].strip.startsWith("ord"))
 | 
				
			||||||
		++i;
 | 
							++i;
 | 
				
			||||||
| 
						 | 
					@ -162,7 +295,7 @@ int cmdRestore(RestoreOpts opt)
 | 
				
			||||||
	for (; i < lines.length; ++i)
 | 
						for (; i < lines.length; ++i)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		auto ln = lines[i].strip;
 | 
							auto ln = lines[i].strip;
 | 
				
			||||||
		if (ln.length == 0 || ln[0] == '#')
 | 
							if (ln.length == 0 || ln[0] == '#' || ln.startsWith("manifest_sha256"))
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		auto cols = ln.split('\t');
 | 
							auto cols = ln.split('\t');
 | 
				
			||||||
| 
						 | 
					@ -178,8 +311,7 @@ int cmdRestore(RestoreOpts opt)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	dst.close();
 | 
						dst.close();
 | 
				
			||||||
	writefln("restore: %s <- %s (chunks: %u)",
 | 
						writefln("restore: %s <- %s (chunks: %u)", opt.outFile, realManifest, cast(uint) count);
 | 
				
			||||||
		opt.outFile, opt.manifestFile, cast(uint) count);
 | 
					 | 
				
			||||||
	return 0;
 | 
						return 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -189,12 +321,13 @@ int cmdRestore(RestoreOpts opt)
 | 
				
			||||||
void printHelp(string prog)
 | 
					void printHelp(string prog)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	writeln("Usage:");
 | 
						writeln("Usage:");
 | 
				
			||||||
	writeln("    ", prog, " split   --store <dir> <file> [--min N] [--avg N] [--max N]");
 | 
						writeln("  ",
 | 
				
			||||||
	writeln("    ", prog, " restore --store <dir> <manifest> <out_file>");
 | 
							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
 | 
					int main(string[] args)
 | 
				
			||||||
{
 | 
					{ // без @safe: getopt берёт &var
 | 
				
			||||||
	if (args.length < 2)
 | 
						if (args.length < 2)
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		printHelp(args[0]);
 | 
							printHelp(args[0]);
 | 
				
			||||||
| 
						 | 
					@ -207,13 +340,16 @@ int main(string[] args) // без @safe: getopt требует &var
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			SplitOpts opt;
 | 
								SplitOpts opt;
 | 
				
			||||||
			string store;
 | 
								string store;
 | 
				
			||||||
			size_t minS = 0, avgS = 0, maxS = 0;
 | 
								string profile;
 | 
				
			||||||
 | 
								size_t minS = 0, avgS = 0, maxS = 0, logEvery = 256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			auto res = getopt(args,
 | 
								auto res = getopt(args,
 | 
				
			||||||
				"store", &store,
 | 
									"store", &store,
 | 
				
			||||||
 | 
									"profile", &profile,
 | 
				
			||||||
				"min", &minS,
 | 
									"min", &minS,
 | 
				
			||||||
				"avg", &avgS,
 | 
									"avg", &avgS,
 | 
				
			||||||
				"max", &maxS
 | 
									"max", &maxS,
 | 
				
			||||||
 | 
									"log-every", &logEvery
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			if (res.helpWanted)
 | 
								if (res.helpWanted)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
| 
						 | 
					@ -221,7 +357,6 @@ int main(string[] args) // без @safe: getopt требует &var
 | 
				
			||||||
				return 0;
 | 
									return 0;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// после getopt в args остаются позиционные
 | 
					 | 
				
			||||||
			if (args.length < 3 || store.length == 0)
 | 
								if (args.length < 3 || store.length == 0)
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				printHelp(args[0]);
 | 
									printHelp(args[0]);
 | 
				
			||||||
| 
						 | 
					@ -230,12 +365,14 @@ int main(string[] args) // без @safe: getopt требует &var
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			opt.storeDir = store;
 | 
								opt.storeDir = store;
 | 
				
			||||||
			opt.filePath = args[2];
 | 
								opt.filePath = args[2];
 | 
				
			||||||
 | 
								opt.profile = profile;
 | 
				
			||||||
			if (minS)
 | 
								if (minS)
 | 
				
			||||||
				opt.minSize = minS;
 | 
									opt.minSize = minS;
 | 
				
			||||||
			if (avgS)
 | 
								if (avgS)
 | 
				
			||||||
				opt.avgSize = avgS;
 | 
									opt.avgSize = avgS;
 | 
				
			||||||
			if (maxS)
 | 
								if (maxS)
 | 
				
			||||||
				opt.maxSize = maxS;
 | 
									opt.maxSize = maxS;
 | 
				
			||||||
 | 
								opt.logEvery = logEvery;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return cmdSplit(opt);
 | 
								return cmdSplit(opt);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -259,7 +396,7 @@ int main(string[] args) // без @safe: getopt требует &var
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			opt.storeDir = store;
 | 
								opt.storeDir = store;
 | 
				
			||||||
			opt.manifestFile = args[2];
 | 
								opt.manifestFile = args[2]; // можно передать *.latest
 | 
				
			||||||
			opt.outFile = args[3];
 | 
								opt.outFile = args[3];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return cmdRestore(opt);
 | 
								return cmdRestore(opt);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue