1f954876a8847da234e0c89b3b43390b40c6ca4e chmalee Tue Feb 25 10:45:31 2025 -0800 Special handle the parentDir setting during uploads by cgi-encoding the components of parentDir but leaving the slash characters in place. This allows hubtools uploads to preserve the filesystem layout on the host on our end, but prevent writing of files outside of the users hubspace directory. Any '..' characters in the parentDir are simply ignored. This commit also fixes some other hubtools related UI bugs, like preventing infinite recursion when doing a row select, displaying file names unencoded, and fixing a bug when deleting subdirectories, refs #31058 diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 8a4da8837fa..152de77e97a 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -333,91 +333,104 @@ // will hold all the information we want to keep visible in the info div let data = {}; // get all of the currently selected rows (may be more than just the one that // was most recently clicked) table.rows({selected: true}).data().each(function(row, ix) { data[ix] = row; }); updateSelectedFileDiv(data); } function createOneCrumb(table, dirName, dirFullPath) { // make a new span that can be clicked to nav through the table let newSpan = document.createElement("span"); newSpan.id = dirName; - newSpan.textContent = dirName; + newSpan.textContent = decodeURIComponent(dirName); newSpan.classList.add("breadcrumb"); newSpan.addEventListener("click", function(e) { dataTableShowDir(table, dirName, dirFullPath); + table.draw(); dataTableCustomOrder(table, {"fullPath": dirFullPath}); table.draw(); }); return newSpan; } function dataTableEmptyBreadcrumb(table) { let currBreadcrumb = document.getElementById("breadcrumb"); currBreadcrumb.replaceChildren(currBreadcrumb.firstChild); currBreadcrumb.firstChild.style.cursor = "unset"; currBreadcrumb.firstChild.style.textDecoration = "unset"; } function dataTableCreateBreadcrumb(table, dirName, dirFullPath) { // Re-create the breadcrumb nav to move back through directories let currBreadcrumb = document.getElementById("breadcrumb"); // empty the node but leave the first "My Data" span if (currBreadcrumb.children.length > 1) { currBreadcrumb.replaceChildren(currBreadcrumb.firstChild); } currBreadcrumb.firstChild.style.cursor = "pointer"; currBreadcrumb.firstChild.style.textDecoration = "underline"; let components = dirFullPath.split("/"); components.forEach(function(dirName, dirNameIx) { if (!dirName) { return; } let path = components.slice(0, dirNameIx+1); componentFullPath = path.join('/'); - componentFullPath += "/"; // need a final '/' on there let newSpan = createOneCrumb(table, dirName, componentFullPath); currBreadcrumb.appendChild(document.createTextNode(" > ")); currBreadcrumb.appendChild(newSpan); }); } // search related functions: function clearSearch(table) { // clear any fixed searches so we can apply a new one let currSearches = table.search.fixed().toArray(); currSearches.forEach((name) => table.search.fixed(name, null)); } function dataTableShowTopLevel(table) { // show all the "root" files, which are files (probably mostly directories) // with no parentDir clearSearch(table); table.search.fixed("showRoot", function(searchStr, rowData, rowIx) { return !rowData.parentDir; }); } function dataTableShowDir(table, dirName, dirFullPath) { // show the directory and all immediate children of the directory clearSearch(table); + table.draw(); table.search.fixed("oneHub", function(searchStr, rowData, rowIx) { - return rowData.parentDir === dirName || rowData.fullPath === dirFullPath; + // calculate the fullPath of this rows parentDir in case the dirName passed + // to this function has the same name as a parentDir further up in the + // listing. For example, consider a test/test/tmp.txt layout, where "test" + // is the parentDir of tmp.txt and the test subdirectory + let parentDirFull = rowData.fullPath.split("/").slice(0,-1).join("/"); + if (rowData.parentDir === dirName && parentDirFull === dirFullPath) { + return true; + } else if (rowData.fullPath === dirFullPath) { + // also return the directory itself + return true; + } else { + return false; + } }); dataTableCreateBreadcrumb(table, dirName, dirFullPath); } // when we move into a new directory, we remove the row from the table // and add it's html into the header, keep the row object around so // we can add it back in later let oldRowData = null; function dataTableCustomOrder(table, dirData) { // figure out the order the rows of the table should be in // if dirData is null, sort on uploadTime first // if dirData exists, that is the first row, followed by everything else // in uploadTime order if (!dirData) { // make sure the old row can show up again in the table @@ -605,44 +618,46 @@ // so we don't need to reparse all files each time we add one } } // show all the new rows we just added, note the double draw, we need // to have the new rows rendered to do the order because the order // will copy the actual DOM node parseFileListIntoHash(uiState.fileList); dataTableShowDir(table, hubDirData.fileName, hubDirData.fullPath); table.draw(); dataTableCustomOrder(table, hubDirData); table.draw(); } function doRowSelect(ev, table, indexes) { - let row = table.row(indexes); - let rowTr = row.node(); + let selectedRow = table.row(indexes); + let rowTr = selectedRow.node(); if (rowTr) { let rowCheckbox = rowTr.childNodes[0].firstChild; - if (rowTr.classList.contains("parentRow")) { + let rowChildren = uiState.filesHash[selectedRow.data().fullPath].children; // we need to manually check the children - table.rows((idx,rowData) => rowData.fullPath.startsWith(row.data().fullPath) && rowData.parentDir === row.data().fileName).every(function(rowIdx, tableLoop, rowLoop) { + if (rowChildren) { + for (let child of rowChildren) { if (ev.type === "select") { - this.select(); + table.row((idx,data) => data.fullPath === child.fullPath).select(); } else { - this.deselect(); + table.row((idx,data) => data.fullPath === child.fullPath).deselect(); } - }); } + } + // lastly check the row itself rowCheckbox.checked = ev.type === "select"; handleCheckboxSelect(ev, table, rowTr); } } function indentActionButton(rowTr, rowData) { let numIndents = "0px"; //data.parentDir !== "" ? data.fullPath.split('/').length - 1: 0; if (rowData.fileType !== "dir") { numIndents = "10px"; } rowTr.childNodes[1].style.textIndent = numIndents; } let tableInitOptions = { select: { @@ -679,30 +694,36 @@ columnDefs: [ { orderable: false, targets: 0, render: DataTable.render.select(), }, { orderable: false, targets: 1, data: "action", title: "", render: function(data, type, row) { if (type === "display") { return dataTablePrintAction(row); } return ''; } }, + { + targets: 2, + render: function(data, type, row, meta) { + return decodeURIComponent(data); + } + }, { targets: 3, render: function(data, type, row, meta) { if (type === "display") { return dataTablePrintSize(data, type, row, meta); } return data; } }, { targets: 5, render: function(data, type, row) { if (type === "display") { return dataTablePrintGenome(data); } @@ -1093,30 +1114,30 @@ "fileName": "hub.txt", "fileSize": 0, "fileType": "hub.txt", "genome": metadata.genome, "parentDir": metadata.parentDir, "fullPath": metadata.parentDir + "/hub.txt", }; parentDirObj = { "uploadTime": now.toLocaleString(), "lastModified": d.toLocaleString(), "fileName": metadata.parentDir, "fileSize": 0, "fileType": "dir", "genome": metadata.genome, "parentDir": "", - "fullPath": metadata.parentDir, + "fullPath": metadata.parentDir + "/", }; // package the three objects together as one "hub" and display it let hub = [parentDirObj, hubTxtObj, newReqObj]; addNewUploadedHubToTable(hub); }); uppy.on('complete', (result) => { history.replaceState(uiState, "", document.location.href); console.log("replace history with uiState"); }); } return { init: init, uiState: uiState, }; }());