1c9352a7c5c2550a52b1d9fb2bae806b6aaed1be chmalee Thu Jan 18 13:28:56 2024 -0800 Starting on pre-create hook for tusd diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 39a0f6d..3fb8d9f 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -1,419 +1,425 @@ /* jshint esnext: true */ debugCartJson = true; function prettyFileSize(num) { if (num < (1000 * 1024)) { return `${(num/1000).toFixed(1)}kb`; } else if (num < (1000 * 1000 * 1024)) { return `${((num/1000)/1000).toFixed(1)}mb`; } else { return `${(((num/1000)/1000)/1000).toFixed(1)}gb`; } } var hubCreate = (function() { let uiState = { // our object for keeping track of the current UI and what to do toUpload: {}, // set of file objects keyed by name input: null, // the <input> element for picking files from the users machine pickedList: null, // the <div> for displaying files in toUpload pendingQueue: [], // our queue of pending [xmlhttprequests, file], kind of like the toUpload object }; // We can use XMLHttpRequest if necessary or a mirror can't use tus - var useTus = tus.isSupported && typeof tusdPort !== 'undefined' && typeof tusdBasePath !== 'undefined'; + var useTus = tus.isSupported && true; function getTusdEndpoint() { // return the port and basepath of the tusd server // NOTE: the port and basepath are specified in hg.conf - let currUrl = parseUrl(window.location.href); - return "https://hgwdev-chmalee.gi.ucsc.edu" + ":" + tusdPort + "/" + tusdBasePath + "/"; + //let currUrl = parseUrl(window.location.href); + return "https://hgwdev-hubspace.gi.ucsc.edu/files"; } function togglePickStateMessage(showMsg = false) { if (showMsg) { let para = document.createElement("p"); para.textContent = "No files selected for upload"; para.classList.add("noFiles"); uiState.pickedList.prepend(para); } else { let msg = document.querySelector(".noFiles"); if (msg) { msg.parentNode.removeChild(msg); } } } function liForFile(file) { let liId = `${file.name}#li`; let li = document.getElementById(liId); return li; } function newButton(text) { /* Creates a new button with some text as the label */ let newBtn = document.createElement("label"); newBtn.classList.add("button"); newBtn.textContent = text; return newBtn; } function requestsPending() { /* Return true if requests are still pending, which means it needs to * have been sent(). aborted requests are considered done for this purpose */ for (let [req, f] of uiState.pendingQueue) { if (req.readyState != XMLHttpRequest.DONE && req.readyState != XMLHttpRequest.UNSENT) { return true; } } return false; } function addCancelButton(file, req) { /* Add a button that cancels the request req */ let li = liForFile(file); let newBtn = newButton("Cancel upload"); newBtn.addEventListener("click", (e) => { req.abort(); li.removeChild(newBtn); // TODO: make this remove the cancel all button if it's the last pending // request stillPending = requestsPending(); if (!stillPending) { let btnRow = document.getElementById("chooseAndSendFilesRow"); cAllBtn = btnRow.lastChild; btnRow.removeChild(cAllBtn); } }); li.append(newBtn); } function removeCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); if (btnRow.lastChild.textContent === "Cancel all") { btnRow.removeChild(btnRow.lastChild); } togglePickStateMessage(true); } function addCancelAllButton() { let btnRow = document.getElementById("chooseAndSendFilesRow"); let newBtn = newButton("Cancel all"); newBtn.addEventListener("click", (e) => { while (uiState.pendingQueue.length > 0) { let [q, f] = uiState.pendingQueue.pop(); // we only need to abort requests that haven't finished yet if (q.readyState != XMLHttpRequest.DONE) { q.abort(); } let li = liForFile(f); if (li !== null) { // the xhr event handlers should handle this but just in case li.removeChild(li.lastChild); } } }); btnRow.appendChild(newBtn); } function makeNewProgMeter(fileName) { // create a progress meter for this filename const progMeterWidth = 128; const progMeterHeight = 12; const progMeter = document.createElement("canvas"); progMeter.classList.add("upload-progress"); progMeter.setAttribute("width", progMeterWidth); progMeter.setAttribute("height", progMeterHeight); idStr = `${fileName}#li`; ele = document.getElementById(idStr); ele.appendChild(progMeter); progMeter.ctx = progMeter.getContext('2d'); progMeter.ctx.fillStyle = 'orange'; progMeter.updateProgress = (percent) => { // update the progress meter for this elem if (percent === 100) { progMeter.ctx.fillStyle = 'green'; } progMeter.ctx.fillRect(0, 0, (progMeterWidth * percent) / 100, progMeterHeight); }; progMeter.updateProgress(0); return progMeter; } function sendFile(file) { // this function can go away once tus is implemented // this is mostly adapted from the mdn example: // https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#example_uploading_a_user-selected_file // we will still need all the event handlers though const xhr = new XMLHttpRequest(); this.progMeter = makeNewProgMeter(file.name); this.xhr = xhr; const endpoint = "../cgi-bin/hgHubConnect"; const self = this; // why do we need this? const fd = new FormData(); this.xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { const pct = Math.round(e.loaded * 100) / e.total; self.progMeter.updateProgress(pct); } }, false); // loadend handles abort/error or load events this.xhr.upload.addEventListener("loadend", (e) => { this.progMeter.updateProgress(); const canvas = self.progMeter.ctx.canvas; canvas.parentNode.removeChild(canvas); delete uiState.toUpload[file.name]; let li = liForFile(file); li.parentNode.removeChild(li); if (Object.keys(uiState.toUpload).length === 0) { removeCancelAllButton(); } }, false); // for now just treat it like an abort/error this.xhr.upload.addEventListener("timeout", (e) => { progMeter.updateProgress(); const canvas = self.progMeter.ctx.canvas; canvas.parentNode.removeChild(canvas); }); this.xhr.upload.addEventListener("load", (e) => { // TODO: on load populate the uploaded files side let uploadSection = document.getElementById("uploadedFilesSection"); if (uploadSection.style.display === "none") { uploadSection.style.display = ""; } }); // on error keep the file name present and show the error somehow this.xhr.upload.addEventListener("error", (e) => { }); this.xhr.upload.addEventListener("abort", (e) => { console.log("request aborted"); }); this.xhr.upload.addEventListener("timeout", (e) => { }); // finally we can send the request this.xhr.open("POST", endpoint, true); fd.set("createHub", 1); fd.set("userFile", file); this.xhr.send(fd); uiState.pendingQueue.push([this.xhr,file]); addCancelButton(file, this.xhr); } function submitPickedFiles() { let tusdServer = getTusdEndpoint(); + let onBeforeRequest = function(req) { + let xhr = req.getUnderlyingObject(req); + xhr.withCredentials = true; + }; for (let f in uiState.toUpload) { file = uiState.toUpload[f]; if (useTus) { let tusOptions = { endpoint: tusdServer, metadata: { filename: file.name, fileType: file.type, fileSize: file.size - } + }, + onBeforeRequest: onBeforeRequest, + retryDelays: [1000], }; // TODO: get the uploadUrl from the tusd server // use a pre-create hook to validate the user // and get an uploadUrl let tusUpload = new tus.Upload(file, tusOptions); tusUpload.start(); } else { // make a new XMLHttpRequest for each file, if tusd-tusclient not supported new sendFile(file); } } addCancelAllButton(); return; } function clearPickedFiles() { while (uiState.pickedList.firstChild) { uiState.pickedList.removeChild(uiState.pickedList.firstChild); } uiState.input.value = ""; uiState.toUpload = {}; togglePickStateMessage(true); } function addClearSubmitButtons() { let firstBtn = document.getElementById("btnForInput"); let btnRow = document.getElementById("chooseAndSendFilesRow"); if (!document.getElementById("clearPicked")) { let newLabel = document.createElement("label"); newLabel.classList.add("button"); newLabel.id = "clearPicked"; newLabel.textContent = "Clear"; newLabel.addEventListener("click", clearPickedFiles); btnRow.append(newLabel); } if (!document.getElementById("submitPicked")) { newLabel = document.createElement("label"); newLabel.id = "submitPicked"; newLabel.classList.add("button"); newLabel.textContent = "Submit"; newLabel.addEventListener("click", submitPickedFiles); btnRow.append(newLabel); } } function removeClearSubmitButtons() { let btn = document.getElementById("clearPicked"); btn.parentNode.removeChild(btn); btn = document.getElementById("submitPicked"); btn.parentNode.removeChild(btn); } function listPickedFiles() { if (uiState.input.files.length === 0) { togglePickStateMessage(true); removeClearSubmitButtons(); } else { let displayList = document.createElement("ul"); displayList.classList.add("pickedFiles"); uiState.pickedList.appendChild(displayList); for (let file of uiState.input.files ) { if (file.name in uiState.toUpload) { continue; } // create a list for the user to see let li = document.createElement("li"); li.classList.add("pickedFile"); li.id = `${file.name}#li`; li.textContent = `File name: ${file.name}, file size: ${prettyFileSize(file.size)}`; displayList.appendChild(li); // finally add it for us uiState.toUpload[file.name] = file; } togglePickStateMessage(false); addClearSubmitButtons(); } } function checkJsonData(jsonData, callerName) { // Return true if jsonData isn't empty and doesn't contain an error; // otherwise complain on behalf of caller. if (! jsonData) { alert(callerName + ': empty response from server'); } else if (jsonData.error) { console.error(jsonData.error); alert(callerName + ': error from server: ' + jsonData.error); } else if (jsonData.warning) { alert("Warning: " + jsonData.warning); return true; } else { if (debugCartJson) { console.log('from server:\n', jsonData); } return true; } return false; } function updateStateAndPage(jsonData, doSaveHistory) { // Update uiState with new values and update the page. _.assign(uiState, jsonData); /* urlVars = {"db": db, "search": uiState.search, "showSearchResults": ""}; // changing the url allows the history to be associated to a specific url var urlParts = changeUrl(urlVars); if (doSaveHistory) saveHistory(uiState, urlParts); changeSearchResultsLabel(); */ } function handleRefreshState(jsonData) { if (checkJsonData(jsonData, 'handleRefreshState')) { updateStateAndPage(jsonData, true); } $("#spinner").remove(); } function init() { cart.setCgi('hgMyData'); cart.debug(debugCartJson); if (!useTus) { console.log("tus is not supported, falling back to XMLHttpRequest"); } let pickedFiles = document.getElementById("fileList"); let input = document.getElementById("uploadedFiles"); if (pickedFiles !== null) { // this element should be an empty div upon loading the page uiState.pickedList = pickedFiles; if (pickedFiles.children.length === 0) { let para = document.createElement("p"); para.textContent = "No files chosen yet"; para.classList.add("noFiles"); pickedFiles.appendChild(para); } } else { // TODO: graceful handle of leaving the page and coming back? } if (input !== null) { uiState.input = input; uiState.input.style.float = "right"; uiState.input.style.opacity = 0; uiState.input.value = ""; } else { let parent = document.getElementById("chooseAndSendFilesRow"); input = document.createElement("input"); input.style.opacity = 0; input.style.float = "right"; input.multiple = true; input.id = "uploadedFiles"; input.type = "file"; uiState.input = input; parent.appendChild(input); } if (typeof cartJson !== "undefined") { if (typeof cartJson.warning !== "undefined") { alert("Warning: " + cartJson.warning); } var urlParts = {}; if (debugCartJson) { console.log('from server:\n', cartJson); } _.assign(uiState,cartJson); saveHistory(cartJson, urlParts, true); } else { // no cartJson object means we are coming to the page for the first time: //cart.send({ getUiState: {} }, handleRefreshState); //cart.flush(); // TODO: initialize buttons, check if there are already files // TODO: write functions for // after picking files // choosing file types // creating default trackDbs // editing trackDbs // TODO: make hgHubConnect respond to requests // TODO: initialize tus-client // TODO: get user name // TODO: send a request with username // TODO: have tusd respond on server input.addEventListener("change", listPickedFiles); // TODO: add event handler for when file is succesful upload // TODO: add event handlers for editing defaults, grouping into hub // TODO: display quota somewhere // TODO: customize the li to remove the picked file } } return { init: init, uiState: uiState, }; }()); // when a user reaches this page from the back button we can display our saved state // instead of sending another network request window.onpopstate = function(event) { event.preventDefault(); hubCreate.updateStateAndPage(event.state, false); };