From 6476f7543d81ec530e1456286eb1d2c8ab55808d Mon Sep 17 00:00:00 2001 From: "L. Bradley LaBoon" Date: Wed, 26 May 2021 19:44:30 -0400 Subject: [PATCH] Implement image uploads --- global.js | 28 +++++++- images/images.css | 2 +- images/images.js | 4 ++ images/index.shtml | 4 +- images/upload/index.shtml | 72 +++++++++++++++++++ images/upload/upload.css | 31 ++++++++ images/upload/upload.js | 147 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 images/upload/index.shtml create mode 100644 images/upload/upload.css create mode 100644 images/upload/upload.js diff --git a/global.js b/global.js index 40f94ce..3c59045 100644 --- a/global.js +++ b/global.js @@ -645,6 +645,32 @@ function oauthPost(endpoint, data, callback) xmlhttp.send(data.toString()); } +// Make an object storage HTTP PUT request +function objPut(url, data, progress, callback) +{ + var xmlhttp = new XMLHttpRequest(); + xmlhttp.upload.addEventListener("progress", progress); + xmlhttp.open("PUT", url, true); + xmlhttp.setRequestHeader("Content-Type", "application/octet-stream"); + + xmlhttp.onreadystatechange = function() + { + if (xmlhttp.readyState != 4) + return; + + if (xmlhttp.status >= 400) { + console.log("Error " + xmlhttp.status); + console.log("PUT " + url); + alert("An error occurred during file upload!"); + return; + } + + callback(); + }; + + xmlhttp.send(data); +} + // Parse URL parameters function parseParams() { @@ -766,4 +792,4 @@ function translateKernel(slug, element) apiGet("/linode/kernels/" + slug, callback, null); } -export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, md5, migrateETA, oauthPost, oauthScopes, parseParams, setupHeader, eventTitles, timeString, translateKernel }; +export { settings, elements, regionNames, apiDelete, apiGet, apiPost, apiPut, md5, migrateETA, oauthPost, oauthScopes, objPut, parseParams, setupHeader, eventTitles, timeString, translateKernel }; diff --git a/images/images.css b/images/images.css index 59df3f1..163b1fd 100644 --- a/images/images.css +++ b/images/images.css @@ -17,7 +17,7 @@ @import url('/global.css'); -table td:nth-of-type(6) { +table td:nth-of-type(7) { text-align: right; } diff --git a/images/images.js b/images/images.js index e4402a4..801063f 100644 --- a/images/images.js +++ b/images/images.js @@ -49,6 +49,10 @@ import { settings, elements, regionNames, apiGet, parseParams, setupHeader, time label.innerHTML = image.label; row.appendChild(label); + var status = document.createElement("td"); + status.innerHTML = image.status.charAt(0).toUpperCase() + image.status.slice(1).replace(/_/g, " "); + row.appendChild(status); + var size = document.createElement("td"); size.innerHTML = image.size + " MB"; row.appendChild(size); diff --git a/images/index.shtml b/images/index.shtml index 7cbe6a2..2b428ae 100644 --- a/images/index.shtml +++ b/images/index.shtml @@ -32,6 +32,7 @@ along with Linode Manager Classic. If not, see . Image + Status Size Type Created @@ -41,10 +42,11 @@ along with Linode Manager Classic. If not, see . - Loading... + Loading... + diff --git a/images/upload/index.shtml b/images/upload/index.shtml new file mode 100644 index 0000000..57b3ea5 --- /dev/null +++ b/images/upload/index.shtml @@ -0,0 +1,72 @@ + + + + + + LMC - Upload a Custom Image + + + + + + +
+ +
+

Image uploading is currently in beta and is subject to the terms of the Early Adopter Testing Agreement.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Upload a Custom Image
Label
Description
Region + For fastest initial upload, select the region that is geographically closest to you.
+ Once uploaded you will be able to deploy the image to other regions. +
File Select
+
+
+ + diff --git a/images/upload/upload.css b/images/upload/upload.css new file mode 100644 index 0000000..85a77b1 --- /dev/null +++ b/images/upload/upload.css @@ -0,0 +1,31 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +@import url('/global.css'); + +.step-2 { + display: none; +} + +tbody tr td:first-of-type { + font-weight: bold; + text-align: right; +} + +#upload { + padding: 0px 15px 15px; +} diff --git a/images/upload/upload.js b/images/upload/upload.js new file mode 100644 index 0000000..6838984 --- /dev/null +++ b/images/upload/upload.js @@ -0,0 +1,147 @@ +/* + * This file is part of Linode Manager Classic. + * + * Linode Manager Classic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Linode Manager Classic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Linode Manager Classic. If not, see . + */ + +import { settings, elements, regionNames, apiGet, apiPost, objPut, parseParams, setupHeader } from "/global.js"; + +(function() +{ + // Element names specific to this page + elements.description = "description"; + elements.imageFile = "image-file"; + elements.label = "label"; + elements.nextButton = "next-button"; + elements.nextRow = "next-row"; + elements.region = "region"; + elements.step2 = "step-2"; + elements.uploadButton = "upload-button"; + + // Data recieved from API calls + var data = {}; + data.url = ""; + + // Static references to UI elements + var ui = {}; + ui.description = {}; + ui.imageFile = {}; + ui.label = {}; + ui.nextButton = {}; + ui.nextRow = {}; + ui.region = {}; + ui.uploadButton = {}; + + // Callback for regions API call + var displayRegions = function(response) + { + for (var i = 0; i < response.data.length; i++) { + var dc = document.createElement("option"); + dc.value = response.data[i].id; + if (regionNames[response.data[i].id]) + dc.innerHTML = regionNames[response.data[i].id]; + else + dc.innerHTML = response.data[i].id; + ui.region.appendChild(dc); + } + + ui.nextButton.disabled = false; + }; + + // Callback for URL generate API call + var displayUpload = function(response) + { + data.url = response.upload_to; + ui.nextRow.remove(); + ui.uploadButton.disabled = false; + var step2 = document.getElementsByClassName(elements.step2); + for (var i = 0; i < step2.length; i++) + step2[i].style.display = "table-row"; + }; + + // Click handler for next button + var handleNext = function(event) + { + if (event.currentTarget.disabled) + return; + + ui.nextButton.disabled = true; + ui.nextButton.innerHTML = "Please wait..."; + + var req = { + "label": ui.label.value, + "region": ui.region.value + }; + if (ui.description.value.length) + req.description = ui.description.value; + + apiPost("beta/images/upload", req, displayUpload); + }; + + // Click handler for upload button + var handleUpload = function(event) + { + if (event.currentTarget.disabled || !ui.imageFile.files.length) + return; + + ui.uploadButton.disabled = true; + ui.uploadButton.innerHTML = "Uploading..."; + objPut(data.url, ui.imageFile.files[0], uploadProgress, function() + { + location.href = "/images"; + }); + }; + + // Progress monitor for upload + var uploadProgress = function(event) + { + var progress = event.loaded / event.total * 100; + ui.uploadButton.innerHTML = "Uploading..." + progress.toFixed(0) + "%"; + }; + + // Initial setup + var setup = function() + { + // Parse URL parameters + data.params = parseParams(); + + setupHeader(); + + // Highlight the Linodes nav link + var navLinks = document.getElementsByClassName(elements.nav); + for (var i = 0; i < navLinks.length; i++) { + if (navLinks[i].pathname == "/linodes/") + navLinks[i].className = elements.navActive; + } + + // Get element references + ui.description = document.getElementById(elements.description); + ui.imageFile = document.getElementById(elements.imageFile); + ui.label = document.getElementById(elements.label); + ui.nextButton = document.getElementById(elements.nextButton); + ui.nextRow = document.getElementById(elements.nextRow); + ui.region = document.getElementById(elements.region); + ui.uploadButton = document.getElementById(elements.uploadButton); + + // Attach event listeners + ui.nextButton.addEventListener("click", handleNext); + ui.uploadButton.addEventListener("click", handleUpload); + + // Get data from API + apiGet("/regions", displayRegions, null); + }; + + // Attach onload handler + window.addEventListener("load", setup); +})();