diff --git a/.gitignore b/.gitignore index aec56e9..e5f67ac 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ create-schematic-gen-site-test-* *.obj *.lst *.jar -extracts/ \ No newline at end of file +extracts/ +cleaner \ No newline at end of file diff --git a/cleaner.d b/cleaner.d new file mode 100644 index 0000000..3afb35b --- /dev/null +++ b/cleaner.d @@ -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; +} diff --git a/site/files.js b/site/files.js index 0d93e76..1f531d1 100644 --- a/site/files.js +++ b/site/files.js @@ -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 form = document.getElementById("schematic-form"); form.onsubmit = async (e) => { e.preventDefault(); resultContainer.innerHTML = "

Uploading and extracting contents...

"; const data = new FormData(form); + const processingTerminal = data.get("processing-terminal"); try { const response = await fetch("/extracts", { method: "POST", body: data }); - const result = await response.json(); - const extractId = result.extractId; - const url = window.location.origin + "/extracts/" + extractId; - resultContainer.innerHTML = `

Copy this URL, and provide it to the computer: ${url}

`; - form.reset(); + if (response.status === 200) { + const result = await response.json(); + const extractId = result.extractId; + const url = window.location.origin + "/extracts/" + extractId; + resultContainer.innerHTML = `

Copy this URL, and provide it to your computer extraction terminal: ${url}

`; + form.reset(); + } else { + const message = await response.text(); + resultContainer.innerHTML = `

Submission failed: ${message}

`; + } } catch (error) { console.error("Error: " + error); resultContainer.innerHTML = `

An error occurred: ${error}

`; } -}; \ No newline at end of file +}; diff --git a/site/index.html b/site/index.html index d287d61..ad7a793 100644 --- a/site/index.html +++ b/site/index.html @@ -10,10 +10,48 @@

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.

+

Instructions

+
    +
  1. Select a schematic file (from your .minecraft folder's schematics folder) that you'd like to extract materials for.
  2. +
  3. 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.
  4. +
  5. If necessary, click Add a Schematic to add another schematic to the list.
  6. +
  7. If you'd like the system to automatically export items to drives at a particular location, fill in the location's name under Processing Terminal. Otherwise, you'll need to manually start the export by going to a valid terminal and pasting the extract link. Note that this is still a work-in-progress. Don't use it yet.
  8. +
  9. Click Submit to submit your request.
  10. +
+ +
+
- - +
+
+ + + +
+
+ +
+ +
+ +
+ +
+ +
+
diff --git a/source/app.d b/source/app.d index 84af2fc..d9d3b1c 100644 --- a/source/app.d +++ b/source/app.d @@ -5,6 +5,7 @@ import slf4d; import slf4d.default_provider; import std.path; import std.file; +import std.json; const EXTRACTS_DIR = "extracts"; const EXTRACT_FILENAME = "__EXTRACT__.json"; @@ -36,39 +37,93 @@ void handleExtract(ref HttpRequestContext ctx) { immutable UUID extractId = randomUUID(); 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()); if (!exists(extractDir)) { mkdirRecurse(extractDir); } string[] filenames; + uint[] counts; foreach (MultipartElement element; data.elements) { - if (element.filename.isNull) continue; - const filePath = buildPath(extractDir, element.filename.get()); - std.file.write(filePath, element.content); - filenames ~= filePath; + 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; + } } - const extractJsonPath = buildPath(extractDir, EXTRACT_FILENAME); + infoF!"Running extract process on files: %s"(filenames); - Pid pid = spawnProcess(EXTRACT_COMMAND ~ filenames, std.stdio.stdin, File(extractJsonPath, "w")); - int exitCode = wait(pid); + auto extractionResult = execute(EXTRACT_COMMAND ~ filenames); + immutable int exitCode = extractionResult.status; infoF!"Exit code: %d"(exitCode); + rmdirRecurse(extractDir); if (exitCode != 0) { ctx.response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR); - ctx.response.writeBodyString(readText(extractJsonPath)); + 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"); - // 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) { 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"); }