diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 454e54124..364f1f6ca 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -56,7 +56,9 @@ function downloadAssetsList(url) { for (const i in availableAssets[assetType]) { const asset = availableAssets[assetType][i]; const elemId = `assets_install_${assetType}_${i}`; - let element = $('', { type: 'checkbox', id: elemId}) + let element = $('', { id:elemId, type:"button", class:"asset-download-button menu_button"}) + const label = $(""); + element.append(label); //if (DEBUG_TONY_SAMA_FORK_MODE) // assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG @@ -64,17 +66,20 @@ function downloadAssetsList(url) { console.debug(DEBUG_PREFIX,"Checking asset",asset["id"], asset["url"]); if (isAssetInstalled(assetType, asset["id"])) { - console.debug(DEBUG_PREFIX,"installed, checked") - element.prop("disabled",true); - element.prop("checked",true); + console.debug(DEBUG_PREFIX,"installed, checked"); + label.toggleClass("fa-download"); + label.toggleClass("fa-check"); } else { console.debug(DEBUG_PREFIX,"not installed, unchecked") element.prop("checked",false); - element.on("click", function(){ - installAsset(asset["url"], assetType, asset["id"]); - element.prop("disabled",true); + element.on("click", async function(){ element.off("click"); + label.toggleClass("fa-download"); + this.classList.toggle('asset-download-button-loading'); + await installAsset(asset["url"], assetType, asset["id"]); + label.toggleClass("fa-check"); + this.classList.toggle('asset-download-button-loading'); }) } @@ -82,7 +87,7 @@ function downloadAssetsList(url) { $(``) .append(element) - .append(`
${asset["id"]}
`) + .append(`${asset["id"]}`) .appendTo(assetTypeMenu); } assetTypeMenu.appendTo("#assets_menu"); @@ -109,14 +114,15 @@ function isAssetInstalled(assetType,filename) { async function installAsset(url, assetType, filename) { console.debug(DEBUG_PREFIX,"Downloading ",url); - const save_path = "public/assets/"+assetType+"/"+filename; + const category = assetType; try { - const result = await fetch(`/asset_download?url=${url}&save_path=${save_path}`, { + const result = await fetch(`/asset_download?url=${url}&category=${category}&filename=${filename}`, { method: 'POST', headers: getRequestHeaders(), }); - let assets = result.ok ? (await result.json()) : []; - return assets; + if(result.ok) { + console.debug(DEBUG_PREFIX,"Download success.") + } } catch (err) { console.log(err); diff --git a/public/scripts/extensions/assets/style.css b/public/scripts/extensions/assets/style.css index eab568515..bdbac8383 100644 --- a/public/scripts/extensions/assets/style.css +++ b/public/scripts/extensions/assets/style.css @@ -6,4 +6,64 @@ .assets-list-div i { display: flex; flex-direction: row; + align-items: center; + justify-content: left; + padding: 5px; } + +.assets-list-div i span{ + margin-left: 10px; +} + +.asset-download-button { + position: relative; + width: 50px; + padding: 8px 16px; + border: none; + outline: none; + border-radius: 2px; + cursor: pointer; + } + +.asset-download-button:active { + background: #007a63; +} + +.asset-download-button-text { + font: bold 20px "Quicksand", san-serif; + color: #ffffff; + transition: all 0.2s; +} + +.asset-download-button-loading .asset-download-button-text { + visibility: hidden; + opacity: 0; +} + +.asset-download-button-loading::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + border: 4px solid transparent; + border-top-color: #ffffff; + border-radius: 50%; + animation: asset-download-button-loading-spinner 1s ease infinite; +} + +@keyframes asset-download-button-loading-spinner { +from { + transform: rotate(0turn); +} + +to { + transform: rotate(1turn); +} +} + + \ No newline at end of file diff --git a/server.js b/server.js index e7d93e19c..3ac9c2032 100644 --- a/server.js +++ b/server.js @@ -5056,20 +5056,52 @@ app.post('/asset_download', jsonParser, async (request, response) => { const { finished } = require('stream/promises'); const path = require("path"); const url = request.query.url; - const file_path = request.query.save_path; + const inputCategory = request.query.category; + const inputFilename = request.query.filename; + const validCategories = ["bgm","ambient"] + + // Check category + let category = null + for(i of validCategories) + if (i == inputCategory) + category = i + if (category === null) { + console.debug("Bad request: unsuported asset category."); + return response.sendStatus(400); + } + + // Sanitize filename + if (inputFilename.indexOf('\0') !== -1) { + console.debug("Bad request: poisong null bytes in filename."); + return response.sendStatus(400); + } + + if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) { + console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted."); + return response.sendStatus(400); + } + + const safe_input = path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, ''); + const file_path = path.join(directories.assets, category, safe_input) console.debug("Request received to download", url,"to",file_path); - - const downloadFile = (async (url, file_path) => { - const res = await fetch(url); - const destination = path.resolve(file_path); - const fileStream = fs.createWriteStream(destination, { flags: 'wx' }); - await finished(Readable.fromWeb(res.body).pipe(fileStream)); - console.debug("Download finished, file saved to",file_path); - }); + try { + const downloadFile = (async (url, file_path) => { + const res = await fetch(url); + const destination = path.resolve(file_path); + const fileStream = fs.createWriteStream(destination, { flags: 'wx' }); + await finished(Readable.fromWeb(res.body).pipe(fileStream)); + console.debug("Download finished, file saved to",file_path); + }); - downloadFile(url, file_path) + await downloadFile(url, file_path); + response.sendStatus(200); + } + catch(error) { + console.log(error); + response.sendStatus(500); + } });