Added cleaner, finalized extraction workflow.

This commit is contained in:
Andrew Lalis 2023-07-16 20:58:16 -04:00
parent 3d194eb2a0
commit 9156db7017
5 changed files with 156 additions and 23 deletions

3
.gitignore vendored
View File

@ -15,4 +15,5 @@ create-schematic-gen-site-test-*
*.obj *.obj
*.lst *.lst
*.jar *.jar
extracts/ extracts/
cleaner

25
cleaner.d Normal file
View File

@ -0,0 +1,25 @@
/**
* 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() {
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;
}

View File

@ -1,21 +1,35 @@
const form = document.getElementById("schematic-form"); const addInputButton = document.getElementById("add-input-button");
const schematicInputSectionTemplate = document.getElementById("schematic-input-section").cloneNode(true);
const schematicInputSectionContainer = document.getElementById("schematic-input-section").parentElement;
addInputButton.onclick = () => {
const newSection = schematicInputSectionTemplate.cloneNode(true);
schematicInputSectionContainer.appendChild(newSection);
};
const resultContainer = document.getElementById("result-container"); const resultContainer = document.getElementById("result-container");
const form = document.getElementById("schematic-form");
form.onsubmit = async (e) => { form.onsubmit = async (e) => {
e.preventDefault(); e.preventDefault();
resultContainer.innerHTML = "<p>Uploading and extracting contents...</p>"; resultContainer.innerHTML = "<p>Uploading and extracting contents...</p>";
const data = new FormData(form); const data = new FormData(form);
const processingTerminal = data.get("processing-terminal");
try { try {
const response = await fetch("/extracts", { const response = await fetch("/extracts", {
method: "POST", method: "POST",
body: data body: data
}); });
const result = await response.json(); if (response.status === 200) {
const extractId = result.extractId; const result = await response.json();
const url = window.location.origin + "/extracts/" + extractId; const extractId = result.extractId;
resultContainer.innerHTML = `<p>Copy this URL, and provide it to the computer: <em>${url}</em></p>`; const url = window.location.origin + "/extracts/" + extractId;
form.reset(); resultContainer.innerHTML = `<p>Copy this URL, and provide it to your computer extraction terminal: <a href="${url}">${url}</a></p>`;
form.reset();
} else {
const message = await response.text();
resultContainer.innerHTML = `<p>Submission failed: <em>${message}</em></p>`;
}
} catch (error) { } catch (error) {
console.error("Error: " + error); console.error("Error: " + error);
resultContainer.innerHTML = `<p>An error occurred: ${error}</p>`; resultContainer.innerHTML = `<p>An error occurred: ${error}</p>`;
} }
}; };

View File

@ -10,10 +10,48 @@
<p> <p>
Use this site to extract lists of materials from one or more schematics (that were generated by the Create mod), so that you can automatically extract all the materials from an automated storage system. Use this site to extract lists of materials from one or more schematics (that were generated by the Create mod), so that you can automatically extract all the materials from an automated storage system.
</p> </p>
<h3>Instructions</h3>
<ol>
<li>Select a schematic file (from your .minecraft folder's <em>schematics folder</em>) that you'd like to extract materials for.</li>
<li>Select a count, indicating the number of times you'd like to export items. Usually this is just 1, but for trees and smaller structures, you can export for many at a time.</li>
<li>If necessary, click <strong>Add a Schematic</strong> to add another schematic to the list.</li>
<li>If you'd like the system to automatically export items to drives at a particular location, fill in the location's name under <strong>Processing Terminal</strong>. Otherwise, you'll need to manually start the export by going to a valid terminal and pasting the extract link. <em>Note that this is still a work-in-progress. Don't use it yet.</em></li>
<li>Click <strong>Submit</strong> to submit your request.</li>
</ol>
<hr>
<form id="schematic-form"> <form id="schematic-form">
<input id="schematic-file-input" name="schematics" type="file" multiple accept=".nbt" required/> <div>
<button type="submit">Submit</button> <div id="schematic-input-section">
<label>
Schematic File
<input name="schematics" type="file" accept=".nbt"/>
</label>
<label>
Count
<input name="counts" type="number" min="1" max="1000" step="1" value="1" required/>
</label>
<button type="button" onclick="event.target.closest('#schematic-input-section').remove();">Remove</button>
</div>
</div>
<div>
<button type="button" id="add-input-button">Add a Schematic</button>
</div>
<div style="background-color: #f3f3f3; padding: 0.5em 0; margin: 0.5em 0;">
<label>
Processing Terminal (Optional)
<input name="processing-terminal" type="text"/>
<em>Enter the name of the processing terminal you'd like to handle exporting materials for your schematics.</em>
<strong>This is still a work-in-progress!</strong>
</label>
</div>
<button type="submit" style="font-size: large">Submit</button>
</form> </form>
<div id="result-container"> <div id="result-container">
</div> </div>

View File

@ -5,6 +5,7 @@ import slf4d;
import slf4d.default_provider; import slf4d.default_provider;
import std.path; import std.path;
import std.file; import std.file;
import std.json;
const EXTRACTS_DIR = "extracts"; const EXTRACTS_DIR = "extracts";
const EXTRACT_FILENAME = "__EXTRACT__.json"; const EXTRACT_FILENAME = "__EXTRACT__.json";
@ -36,39 +37,93 @@ void handleExtract(ref HttpRequestContext ctx) {
immutable UUID extractId = randomUUID(); immutable UUID extractId = randomUUID();
MultipartFormData data = ctx.request.readBodyAsMultipartFormData(); MultipartFormData data = ctx.request.readBodyAsMultipartFormData();
// TODO: Validate data (unique filenames, no non-file elements, etc.) if (!validateExtractRequest(data, ctx.response)) return;
const extractDir = buildPath(EXTRACTS_DIR, extractId.toString()); const extractDir = buildPath(EXTRACTS_DIR, extractId.toString());
if (!exists(extractDir)) { if (!exists(extractDir)) {
mkdirRecurse(extractDir); mkdirRecurse(extractDir);
} }
string[] filenames; string[] filenames;
uint[] counts;
foreach (MultipartElement element; data.elements) { foreach (MultipartElement element; data.elements) {
if (element.filename.isNull) continue; if (element.name == "schematics") {
const filePath = buildPath(extractDir, element.filename.get()); const filePath = buildPath(extractDir, element.filename.get());
std.file.write(filePath, element.content); std.file.write(filePath, element.content);
filenames ~= filePath; filenames ~= filePath;
} else if (element.name == "counts") {
import std.conv;
immutable uint count = element.content.to!uint;
counts ~= count;
}
} }
const extractJsonPath = buildPath(extractDir, EXTRACT_FILENAME);
infoF!"Running extract process on files: %s"(filenames); infoF!"Running extract process on files: %s"(filenames);
Pid pid = spawnProcess(EXTRACT_COMMAND ~ filenames, std.stdio.stdin, File(extractJsonPath, "w")); auto extractionResult = execute(EXTRACT_COMMAND ~ filenames);
int exitCode = wait(pid); immutable int exitCode = extractionResult.status;
infoF!"Exit code: %d"(exitCode); infoF!"Exit code: %d"(exitCode);
rmdirRecurse(extractDir);
if (exitCode != 0) { if (exitCode != 0) {
ctx.response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); ctx.response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
ctx.response.writeBodyString(readText(extractJsonPath)); ctx.response.writeBodyString(extractionResult.output);
} else { } 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; JSONValue result = JSONValue.emptyObject;
result.object["extractId"] = JSONValue(extractId.toString()); result.object["extractId"] = JSONValue(extractId.toString());
ctx.response.writeBodyString(result.toJSON(), "application/json"); ctx.response.writeBodyString(result.toJSON(), "application/json");
// Remove schematic files after we are done. }
foreach (string schematicFile; filenames) { }
std.file.remove(schematicFile);
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) { void getExtract(ref HttpRequestContext ctx) {
string extractId = ctx.request.getPathParamAs!string("extractId"); string extractId = ctx.request.getPathParamAs!string("extractId");
const extractFile = buildPath(EXTRACTS_DIR, extractId, EXTRACT_FILENAME); const extractFile = buildPath(EXTRACTS_DIR, extractId ~ ".json");
fileResponse(ctx.response, extractFile, "application/json"); fileResponse(ctx.response, extractFile, "application/json");
} }