module csgs.extract; import handy_httpd; import slf4d; import std.typecons; import std.file; import std.path; const EXTRACTS_DIR = "extracts"; const EXTRACT_FILENAME = "__EXTRACT__.json"; const EXTRACT_COMMAND = ["java", "-jar", "materials-extractor-v1.0.0.jar"]; bool validateExtractRequest(ref MultipartFormData data, ref HttpResponse response) { if (data.elements.length < 2) { response.setStatus(HttpStatus.BAD_REQUEST); response.writeBodyString("Requires at least 2 form data elements (schematic file and count)."); return false; } uint nextElementIdx = 0; while (nextElementIdx < data.elements.length) { MultipartElement element = data.elements[nextElementIdx++]; if (element.name == "schematics") { if (element.filename.isNull || element.filename.get().length < 5 || element.content.length < 10) { response.setStatus(HttpStatus.BAD_REQUEST); response.writeBodyString("Invalid or missing schematic file."); return false; } const string filename = element.filename.get(); if (nextElementIdx == data.elements.length) { response.setStatus(HttpStatus.BAD_REQUEST); response.writeBodyString("Missing count element for schematic: " ~ filename); return false; } MultipartElement countElement = data.elements[nextElementIdx++]; import std.conv; try { immutable uint count = countElement.content.to!uint; if (count < 1 || count > 1000) throw new Exception("out of range: count should be between 1 and 1000, inclusive."); } catch (Exception e) { response.setStatus(HttpStatus.BAD_REQUEST); response.writeBodyString("Invalid count element: " ~ e.msg); return false; } } else if (element.name == "processing-terminal") { // TODO: Check processing-terminal format. } else { response.setStatus(HttpStatus.BAD_REQUEST); response.writeBodyString("Unknown element: " ~ element.name); return false; } } return true; } string doExtract(ref MultipartFormData data) { import std.json; import std.uuid; import std.process; immutable UUID extractId = randomUUID(); immutable string extractIdStr = extractId.toString(); immutable string extractDir = buildPath(EXTRACTS_DIR, extractIdStr); if (!exists(extractDir)) { mkdirRecurse(extractDir); } string[] filenames; uint[] counts; foreach (MultipartElement element; data.elements) { if (element.name == "schematics") { const filePath = buildPath(extractDir, element.filename.get()); std.file.write(filePath, element.content); filenames ~= filePath; } else if (element.name == "counts") { import std.conv; immutable uint count = element.content.to!uint; counts ~= count; } } infoF!"Running extract process on files: %s"(filenames); Nullable!string extractorPath = getExtractorProgramPath(); if (extractorPath.isNull) { throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Could not find extractor program."); } auto extractionResult = execute(["java", "-jar", extractorPath.get()] ~ filenames); immutable int exitCode = extractionResult.status; infoF!"Exit code: %d"(exitCode); rmdirRecurse(extractDir); if (exitCode != 0) { throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Extraction program failed."); } else { immutable string extractJsonPath = extractDir ~ ".json"; // It was successful, so add __COUNT__ to each object in the extract data. JSONValue extractData = parseJSON(extractionResult.output); for (uint i = 0; i < extractData.array.length; i++) { extractData.array[i].object["__COUNT__"] = JSONValue(counts[i]); } std.file.write(extractJsonPath, extractData.toPrettyString()); return extractIdStr; } } Nullable!string getExtractorProgramPath() { import std.string; foreach (DirEntry entry; dirEntries(getcwd(), SpanMode.shallow, false)) { if (entry.isFile() && endsWith(entry.name, ".jar")) { return nullable(entry.name); } } return Nullable!string.init; } Nullable!string getCurrentExtractorVersion() { import std.regex; Nullable!string extractorPath = getExtractorProgramPath(); if (extractorPath.isNull) return Nullable!string.init; auto r = regex(`v\d+\.\d+\.\d+`); auto c = matchFirst(extractorPath.get(), r); if (c.empty) return Nullable!string.init; return nullable(c.front()); }