Added scheduled tasks and version management.
This commit is contained in:
parent
50d1d8f662
commit
eebcf0c91a
|
@ -1,7 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
dub clean
|
dub clean
|
||||||
dub build --build=release
|
rm -f create-schematic-gen-site
|
||||||
|
dub build
|
||||||
echo "Stopping CSGS service."
|
echo "Stopping CSGS service."
|
||||||
ssh -f root@andrewlalis.com 'systemctl stop csgs'
|
ssh -f root@andrewlalis.com 'systemctl stop csgs'
|
||||||
echo "Uploading new binary."
|
echo "Uploading new binary."
|
||||||
|
|
26
cleaner.d
26
cleaner.d
|
@ -1,26 +0,0 @@
|
||||||
/**
|
|
||||||
* This standalone module is responsible for cleaning up the list of stored
|
|
||||||
* extracts, so only the recent ones remain. This is meant to be linked as a
|
|
||||||
* cron scheduled program.
|
|
||||||
*/
|
|
||||||
module cleaner;
|
|
||||||
|
|
||||||
import std.stdio;
|
|
||||||
import std.file;
|
|
||||||
import std.path;
|
|
||||||
import std.datetime;
|
|
||||||
|
|
||||||
const EXTRACTS_DIR = "extracts";
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
if (!exists(EXTRACTS_DIR)) return 0;
|
|
||||||
immutable SysTime now = Clock.currTime();
|
|
||||||
foreach (DirEntry entry; dirEntries(EXTRACTS_DIR, SpanMode.shallow, false)) {
|
|
||||||
Duration age = now - entry.timeLastModified();
|
|
||||||
if (age.total!"days" > 5) {
|
|
||||||
writefln!"Removing directory %s because it's too old."(entry.name);
|
|
||||||
rmdirRecurse(entry.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
4
dub.json
4
dub.json
|
@ -4,7 +4,9 @@
|
||||||
],
|
],
|
||||||
"copyright": "Copyright © 2023, Andrew Lalis",
|
"copyright": "Copyright © 2023, Andrew Lalis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handy-httpd": "~>7.6.3",
|
"handy-httpd": "~>7.6.4",
|
||||||
|
"requests": "~>2.1.1",
|
||||||
|
"scheduled": "~>1.2.0",
|
||||||
"slf4d": "~>2.4.2"
|
"slf4d": "~>2.4.2"
|
||||||
},
|
},
|
||||||
"description": "HTTP server for generating schematic materials lists.",
|
"description": "HTTP server for generating schematic materials lists.",
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
{
|
{
|
||||||
"fileVersion": 1,
|
"fileVersion": 1,
|
||||||
"versions": {
|
"versions": {
|
||||||
"handy-httpd": "7.6.3",
|
"automem": "0.6.9",
|
||||||
|
"cachetools": "0.4.1",
|
||||||
|
"cronexp": "0.1.0-beta3",
|
||||||
|
"handy-httpd": "7.6.4",
|
||||||
"httparsed": "1.2.1",
|
"httparsed": "1.2.1",
|
||||||
|
"requests": "2.1.1",
|
||||||
|
"scheduled": "1.1.0",
|
||||||
"slf4d": "2.4.2",
|
"slf4d": "2.4.2",
|
||||||
"streams": "3.5.0"
|
"streams": "3.5.0",
|
||||||
|
"test_allocator": "0.3.4",
|
||||||
|
"unit-threaded": "0.10.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
source/app.d
126
source/app.d
|
@ -1,130 +1,14 @@
|
||||||
import handy_httpd;
|
|
||||||
import handy_httpd.handlers.path_delegating_handler;
|
|
||||||
import handy_httpd.handlers.file_resolving_handler;
|
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import slf4d.default_provider;
|
import slf4d.default_provider;
|
||||||
import std.path;
|
|
||||||
import std.file;
|
|
||||||
import std.json;
|
|
||||||
|
|
||||||
const EXTRACTS_DIR = "extracts";
|
import csgs.http;
|
||||||
const EXTRACT_FILENAME = "__EXTRACT__.json";
|
import csgs.task;
|
||||||
const EXTRACT_COMMAND = ["java", "-jar", "materials-extractor-v1.0.0.jar"];
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
||||||
configureLoggingProvider(provider);
|
configureLoggingProvider(provider);
|
||||||
info("Starting create-schematic-gen-site API.");
|
|
||||||
|
|
||||||
ServerConfig config = ServerConfig.defaultValues();
|
|
||||||
config.workerPoolSize = 3;
|
|
||||||
config.connectionQueueSize = 10;
|
|
||||||
config.port = 8100;
|
|
||||||
|
|
||||||
PathDelegatingHandler handler = new PathDelegatingHandler();
|
|
||||||
handler.addMapping("POST", "/extracts", &handleExtract);
|
|
||||||
handler.addMapping("GET", "/extracts/{extractId}", &getExtract);
|
|
||||||
|
|
||||||
FileResolvingHandler fileHandler = new FileResolvingHandler("site", DirectoryResolutionStrategies.serveIndexFiles);
|
|
||||||
handler.addMapping("/**", fileHandler);
|
|
||||||
new HttpServer(handler, config).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleExtract(ref HttpRequestContext ctx) {
|
|
||||||
import std.json;
|
|
||||||
import std.uuid;
|
|
||||||
import std.process;
|
|
||||||
import std.stdio;
|
|
||||||
|
|
||||||
immutable UUID extractId = randomUUID();
|
|
||||||
MultipartFormData data = ctx.request.readBodyAsMultipartFormData();
|
|
||||||
if (!validateExtractRequest(data, ctx.response)) return;
|
|
||||||
const extractDir = buildPath(EXTRACTS_DIR, extractId.toString());
|
|
||||||
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);
|
info("Starting create-schematic-gen-site API.");
|
||||||
auto extractionResult = execute(EXTRACT_COMMAND ~ filenames);
|
startScheduledTasks();
|
||||||
immutable int exitCode = extractionResult.status;
|
startServer();
|
||||||
infoF!"Exit code: %d"(exitCode);
|
|
||||||
rmdirRecurse(extractDir);
|
|
||||||
if (exitCode != 0) {
|
|
||||||
ctx.response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
|
|
||||||
ctx.response.writeBodyString(extractionResult.output);
|
|
||||||
} else {
|
|
||||||
const 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());
|
|
||||||
|
|
||||||
JSONValue result = JSONValue.emptyObject;
|
|
||||||
result.object["extractId"] = JSONValue(extractId.toString());
|
|
||||||
ctx.response.writeBodyString(result.toJSON(), "application/json");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void getExtract(ref HttpRequestContext ctx) {
|
|
||||||
string extractId = ctx.request.getPathParamAs!string("extractId");
|
|
||||||
const extractFile = buildPath(EXTRACTS_DIR, extractId ~ ".json");
|
|
||||||
fileResponse(ctx.response, extractFile, "application/json");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
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());
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
module csgs.http;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import handy_httpd.handlers.path_delegating_handler;
|
||||||
|
import handy_httpd.handlers.file_resolving_handler;
|
||||||
|
import slf4d;
|
||||||
|
import std.path;
|
||||||
|
|
||||||
|
import csgs.extract;
|
||||||
|
|
||||||
|
void startServer() {
|
||||||
|
ServerConfig config = ServerConfig.defaultValues();
|
||||||
|
config.workerPoolSize = 3;
|
||||||
|
config.connectionQueueSize = 10;
|
||||||
|
config.port = 8100;
|
||||||
|
|
||||||
|
PathDelegatingHandler handler = new PathDelegatingHandler();
|
||||||
|
handler.addMapping("POST", "/extracts", &handleExtract);
|
||||||
|
handler.addMapping("GET", "/extracts/{extractId}", &getExtract);
|
||||||
|
|
||||||
|
FileResolvingHandler fileHandler = new FileResolvingHandler("site", DirectoryResolutionStrategies.serveIndexFiles);
|
||||||
|
handler.addMapping("/**", fileHandler);
|
||||||
|
new HttpServer(handler, config).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleExtract(ref HttpRequestContext ctx) {
|
||||||
|
import std.json;
|
||||||
|
|
||||||
|
MultipartFormData data = ctx.request.readBodyAsMultipartFormData();
|
||||||
|
if (!validateExtractRequest(data, ctx.response)) return;
|
||||||
|
string extractId = doExtract(data);
|
||||||
|
JSONValue result = JSONValue.emptyObject;
|
||||||
|
result.object["extractId"] = JSONValue(extractId);
|
||||||
|
ctx.response.writeBodyString(result.toJSON(), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getExtract(ref HttpRequestContext ctx) {
|
||||||
|
string extractId = ctx.request.getPathParamAs!string("extractId");
|
||||||
|
const extractFile = buildPath(EXTRACTS_DIR, extractId ~ ".json");
|
||||||
|
fileResponse(ctx.response, extractFile, "application/json");
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module csgs;
|
|
@ -0,0 +1,110 @@
|
||||||
|
module csgs.task;
|
||||||
|
|
||||||
|
import slf4d;
|
||||||
|
import scheduled;
|
||||||
|
|
||||||
|
import std.datetime;
|
||||||
|
import std.typecons;
|
||||||
|
import std.json;
|
||||||
|
import std.string;
|
||||||
|
import std.file;
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
import csgs.extract;
|
||||||
|
|
||||||
|
void startScheduledTasks() {
|
||||||
|
JobScheduler scheduler = new ThreadedJobScheduler();
|
||||||
|
Job cleanJob = new FunctionJob(&cleanOldExtracts);
|
||||||
|
scheduler.addJob(cleanJob, new FixedIntervalSchedule(days(1)));
|
||||||
|
Job updateCheckJob = new FunctionJob(&checkForExtractorUpdate);
|
||||||
|
scheduler.addJob(updateCheckJob, new FixedIntervalSchedule(hours(1)));
|
||||||
|
scheduler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanOldExtracts() {
|
||||||
|
import std.file;
|
||||||
|
immutable MAX_AGE = days(5);
|
||||||
|
info("Cleaning old extracts.");
|
||||||
|
if (!exists(EXTRACTS_DIR)) return;
|
||||||
|
immutable SysTime now = Clock.currTime();
|
||||||
|
foreach (DirEntry entry; dirEntries(EXTRACTS_DIR, SpanMode.shallow, false)) {
|
||||||
|
if (entry.isDir) {
|
||||||
|
infoF!"Removing directory %s."(entry.name);
|
||||||
|
rmdirRecurse(entry.name);
|
||||||
|
} else if (entry.isFile) {
|
||||||
|
immutable Duration age = now - entry.timeLastModified();
|
||||||
|
if (age > MAX_AGE) {
|
||||||
|
infoF!"Removing extract %s because it's too old."(entry.name);
|
||||||
|
std.file.remove(entry.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkForExtractorUpdate() {
|
||||||
|
import requests;
|
||||||
|
import std.json;
|
||||||
|
import std.stdio;
|
||||||
|
import std.string;
|
||||||
|
import std.file;
|
||||||
|
info("Checking for MaterialsExtractor program updates.");
|
||||||
|
|
||||||
|
// First request the latest release from GitHub API:
|
||||||
|
Request req = Request();
|
||||||
|
req.addHeaders(["Accept": "application/vnd.github+json"]);
|
||||||
|
Response resp = req.get("https://api.github.com/repos/andrewlalis/MaterialsExtractor/releases/latest");
|
||||||
|
if (resp.code() != 200) {
|
||||||
|
warnF!"Failed to get the latest MaterialsExtractor release. Status code %d."(resp.code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONValue responseJson = parseJSON(resp.responseBody.toString());
|
||||||
|
|
||||||
|
// Get the release's version, compare it with our current version.
|
||||||
|
immutable string releaseVersion = responseJson.object["tag_name"].str;
|
||||||
|
infoF!"Found release version %s"(releaseVersion);
|
||||||
|
Nullable!string currentVersion = getCurrentExtractorVersion();
|
||||||
|
if (!currentVersion.isNull && currentVersion.get() >= releaseVersion) {
|
||||||
|
infoF!"Skipping this version since it's not newer than the current %s."(currentVersion.get);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the release's asset, and download it.
|
||||||
|
Nullable!JSONValue nullableAsset = findJarAsset(responseJson);
|
||||||
|
if (nullableAsset.isNull) {
|
||||||
|
warn("Latest release has no asset. Ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONValue asset = nullableAsset.get();
|
||||||
|
immutable string filename = asset.object["name"].str;
|
||||||
|
immutable string downloadUrl = asset.object["browser_download_url"].str;
|
||||||
|
infoF!"Found asset: %s. Downloading from %s."(filename, downloadUrl);
|
||||||
|
if (exists(filename)) {
|
||||||
|
std.file.remove(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
Request downloadRequest = Request();
|
||||||
|
downloadRequest.useStreaming = true;
|
||||||
|
Response downloadResponse = downloadRequest.get(downloadUrl);
|
||||||
|
File f = File(filename, "wb");
|
||||||
|
auto stream = downloadResponse.receiveAsRange();
|
||||||
|
while (!stream.empty) {
|
||||||
|
f.rawWrite(stream.front);
|
||||||
|
stream.popFront();
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
infoF!"Downloaded file to %s"(f.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Nullable!JSONValue findJarAsset(ref JSONValue response) {
|
||||||
|
foreach (JSONValue asset; response.object["assets"].array) {
|
||||||
|
immutable string name = asset.object["name"].str;
|
||||||
|
if (
|
||||||
|
startsWith(name, "materials-extractor") &&
|
||||||
|
endsWith(name, ".jar") &&
|
||||||
|
asset.object["content_type"].str == "application/java-archive"
|
||||||
|
) {
|
||||||
|
return nullable(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Nullable!JSONValue.init;
|
||||||
|
}
|
Loading…
Reference in New Issue