diff --git a/CHANGELOG.md b/CHANGELOG.md index 55ed6f2..847f661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -## Cockpit Navigator 0.5.3-1 +## Cockpit Navigator 0.5.4-1 -* Implement inline filename editing. -* Add information popup button. \ No newline at end of file +* Add fuzzy search. +* Optimize folder uploads. +* Fix bugs with selecting entries and renaming files. +* Stop user from deleting or renaming system-critical paths. \ No newline at end of file diff --git a/README.md b/README.md index 50db70f..e6b1cb7 100644 --- a/README.md +++ b/README.md @@ -23,28 +23,28 @@ With no command line use needed, you can: # Installation ## From Github Release ### Ubuntu -1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator_0.5.3-1focal_all.deb` -1. `# apt install ./cockpit-navigator_0.5.3-1focal_all.deb` +1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator_0.5.4-1focal_all.deb` +1. `# apt install ./cockpit-navigator_0.5.4-1focal_all.deb` ### EL7 -1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator-0.5.3-1.el7.noarch.rpm` +1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator-0.5.4-1.el7.noarch.rpm` ### EL8 -1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator-0.5.3-1.el8.noarch.rpm` +1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator-0.5.4-1.el8.noarch.rpm` ## From Source 1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`, `zip`. 1. `$ git clone https://github.com/45Drives/cockpit-navigator.git` 1. `$ cd cockpit-navigator` -1. `$ git checkout ` (v0.5.2 is latest) +1. `$ git checkout ` (v0.5.4 is latest) 1. `# make install` ## From 45Drives Repositories ### Ubuntu 1. Import GPG Key ```sh -wget -qO - http://repo.45drives.com/key.asc | sudo apt-key add - +wget -qO - https://repo.45drives.com/key/gpg.asc | gpg --dearmor -o /usr/share/keyrings/45drives-archive-keyring.gpg ``` -2. Add 45drives.list +2. Add 45drives.sources ```sh cd /etc/apt/sources.list.d -sudo wget http://repo.45drives.com/debian/45drives.list +curl -sSL https://repo.45drives.com/lists/45drives.sources -o /etc/apt/sources.list.d/45drives.sources sudo apt update ``` 3. Install Package @@ -52,10 +52,9 @@ sudo apt update sudo apt install cockpit-navigator ``` ### EL7/EL8 -1. Add Repository +1. Add 45drives.repo ```sh -cd /etc/yum.repos.d -sudo wget http://repo.45drives.com/rhel/45drives.repo +curl -sSL https://repo.45drives.com/lists/45drives.repo -o /etc/yum.repos.d/45drives.repo sudo yum clean all ``` 2. Install Package diff --git a/manifest.json b/manifest.json index 340b1c8..da34e60 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "cockpit-navigator", "title": "Cockpit Navigator", "prerelease": false, - "version": "0.5.3", + "version": "0.5.4", "buildVersion": "1", "author": "Josh Boudreau ", "url": "https://github.com/45Drives/cockpit-navigator", @@ -54,7 +54,7 @@ ], "changelog": { "urgency": "medium", - "version": "0.5.3", + "version": "0.5.4", "buildVersion": "1", "ignore": [], "date": null, diff --git a/navigator/components/FileUpload.js b/navigator/components/FileUpload.js index c27bf75..e5877f3 100644 --- a/navigator/components/FileUpload.js +++ b/navigator/components/FileUpload.js @@ -41,34 +41,30 @@ export class FileUpload { this.reader = new FileReader(); this.chunks = this.slice_file(file); this.chunk_index = 0; - this.timestamp = Date.now(); this.modal_prompt = new ModalPrompt(); this.using_webkit = true; - } - - check_if_exists() { - return new Promise((resolve, reject) => { - var proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/fail-if-exists.py3", this.path], {superuser: "try"}); - proc.done((data) => {resolve(false)}); - proc.fail((e, data) => {resolve(true)}); - }); + this.make_html_element(); } make_html_element() { - var notification = document.createElement("div"); + var notification = this.dom_element = document.createElement("div"); notification.classList.add("nav-notification"); var header = document.createElement("div"); header.classList.add("nav-notification-header"); notification.appendChild(header); - header.innerText = "Uploading " + this.filename; - header.style.position = "relative"; - header.style.paddingRight = "1em"; + header.style.display = "grid"; + header.style.gridTemplateColumns = "1fr 20px"; + header.style.gap = "5px"; + + var title = document.createElement("p"); + title.innerText = "Uploading " + this.filename; + title.title = this.filename; var cancel = document.createElement("i"); cancel.classList.add("fa", "fa-times"); - cancel.style.position = "absolute" - cancel.style.right = "0"; + cancel.style.justifySelf = "center"; + cancel.style.alignSelf = "center"; cancel.style.cursor = "pointer"; cancel.onclick = () => { if (this.proc) { @@ -76,7 +72,8 @@ export class FileUpload { this.done(); } } - header.appendChild(cancel); + + header.append(title, cancel); var info = document.createElement("div"); info.classList.add("flex-row", "space-between"); @@ -129,7 +126,8 @@ export class FileUpload { } async upload() { - this.make_html_element(); + this.timestamp = Date.now(); + this.dom_element.style.display = "flex"; this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py3", this.path], {err: "out", superuser: "try"}); this.proc.fail((e, data) => { this.reader.onload = () => {} @@ -137,8 +135,10 @@ export class FileUpload { this.nav_window_ref.modal_prompt.alert(e, data); }) this.proc.done((data) => { - this.nav_window_ref.refresh(); + if (!this.done_hook) + this.nav_window_ref.refresh(); }) + this.proc.always(() => this?.done_hook?.()); this.reader.onerror = (evt) => { this.modal_prompt.alert("Failed to read file: " + this.filename, "Upload of directories not supported."); this.done(); @@ -164,6 +164,7 @@ export class FileUpload { } this.done(); } + this.update_rates_interval = setInterval(this.display_xfr_rate.bind(this), 1000); } /** @@ -199,6 +200,7 @@ export class FileUpload { done() { this.proc.input(); // close stdin this.remove_html_element(); + clearInterval(this.update_rates_interval); } update_xfr_rate() { @@ -206,12 +208,19 @@ export class FileUpload { var elapsed = (now - this.timestamp) / 1000; this.timestamp = now; var rate = this.chunk_size / elapsed; - this.rate.innerText = cockpit.format_bytes_per_sec(rate); + this.rate_avg = (this.rate_avg) + ? (0.125 * rate + (0.875 * this.rate_avg)) + : rate; // keep exponential moving average of chunk time for eta this.chunk_time = (this.chunk_time) ? (0.125 * elapsed + (0.875 * this.chunk_time)) : elapsed; var eta = (this.num_chunks - this.chunk_index) * this.chunk_time; - this.eta.innerText = format_time_remaining(eta); + this.eta_avg = eta; + } + + display_xfr_rate() { + this.rate.innerText = cockpit.format_bytes_per_sec(this.rate_avg); + this.eta.innerText = format_time_remaining(this.eta_avg); } } diff --git a/navigator/components/FileUploadManager.js b/navigator/components/FileUploadManager.js new file mode 100644 index 0000000..22e6f74 --- /dev/null +++ b/navigator/components/FileUploadManager.js @@ -0,0 +1,62 @@ +/* + Cockpit Navigator - A File System Browser for Cockpit. + Copyright (C) 2021 Josh Boudreau + Copyright (C) 2021 Sam Silver + Copyright (C) 2021 Dawson Della Valle + + This file is part of Cockpit Navigator. + Cockpit Navigator 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. + Cockpit Navigator 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 Cockpit Navigator. If not, see . + */ + +import {FileUpload} from "./FileUpload.js"; +import {NavWindow} from "./NavWindow.js"; + +export class FileUploadManager { + /** + * + * @param {NavWindow} nav_window_ref + * @param {number} max_concurrent + */ + constructor(nav_window_ref, max_concurrent = 10) { + this.nav_window_ref = nav_window_ref; + this.running = 0; + this.remaining_uploads = []; + this.max_concurrent = max_concurrent; + this.start_next = () => { + let next_upload = this.remaining_uploads.pop(); + if (next_upload) { + next_upload?.upload?.(); + this.running++; + } + } + } + + /** + * + * @param {...FileUpload} uploads + */ + add(...uploads) { + let done_hook = () => { + this.running--; + this.start_next(); + if (!this.running) + this.nav_window_ref.refresh(); + } + for (let upload of uploads) { + upload.done_hook = done_hook; + this.remaining_uploads.unshift(upload); + } + while (this.remaining_uploads.length && this.running < this.max_concurrent) { + this.start_next(); + } + } +} \ No newline at end of file diff --git a/navigator/components/NavContextMenu.js b/navigator/components/NavContextMenu.js index 1333cf0..9e83295 100644 --- a/navigator/components/NavContextMenu.js +++ b/navigator/components/NavContextMenu.js @@ -72,7 +72,7 @@ export class NavContextMenu { new_link(e) { var default_target = ""; if (this.nav_window_ref.selected_entries.size <= 1 && this.target !== this.nav_window_ref.pwd()) - default_target = this.target.filename(); + default_target = this.target.filename; this.nav_window_ref.ln(default_target); } @@ -90,7 +90,14 @@ export class NavContextMenu { async rename(e) { this.hide(); - this.target.show_edit(this.target.dom_element.nav_item_title); + if (this.target.is_dangerous_path()) { + await this.nav_window_ref.modal_prompt.alert( + "Cannot rename system-critical paths.", + "If you think you need to, use the terminal." + ); + } else { + this.target.show_edit(this.target.dom_element.nav_item_title); + } e.stopPropagation(); } diff --git a/navigator/components/NavDir.js b/navigator/components/NavDir.js index a946120..d77afd0 100644 --- a/navigator/components/NavDir.js +++ b/navigator/components/NavDir.js @@ -213,6 +213,18 @@ export class NavDir extends NavEntry { } super.show_properties(extra_properties); } + + style_selected() { + this.dom_element.nav_item_icon.classList.remove("fa-folder"); + this.dom_element.nav_item_icon.classList.add("fa-folder-open"); + super.style_selected(); + } + + unstyle_selected() { + this.dom_element.nav_item_icon.classList.add("fa-folder"); + this.dom_element.nav_item_icon.classList.remove("fa-folder-open"); + super.unstyle_selected(); + } } export class NavDirLink extends NavDir{ diff --git a/navigator/components/NavDownloader.js b/navigator/components/NavDownloader.js index fa64df7..12b234b 100644 --- a/navigator/components/NavDownloader.js +++ b/navigator/components/NavDownloader.js @@ -26,7 +26,7 @@ export class NavDownloader { */ constructor(file) { this.path = file.path_str(); - this.filename = file.filename(); + this.filename = file.filename; this.read_size = file.stat["size"]; } diff --git a/navigator/components/NavDragDrop.js b/navigator/components/NavDragDrop.js index 4b72dfb..2d3f751 100644 --- a/navigator/components/NavDragDrop.js +++ b/navigator/components/NavDragDrop.js @@ -18,8 +18,9 @@ */ import {FileUpload} from "./FileUpload.js"; -import { ModalPrompt } from "./ModalPrompt.js"; +import {ModalPrompt} from "./ModalPrompt.js"; import {NavWindow} from "./NavWindow.js"; +import {FileUploadManager} from "./FileUploadManager.js"; export class NavDragDrop { /** @@ -35,6 +36,7 @@ export class NavDragDrop { this.drop_area = drop_area; this.nav_window_ref = nav_window_ref; this.modal_prompt = new ModalPrompt(); + this.upload_manager = new FileUploadManager(this.nav_window_ref, 6); } /** @@ -82,7 +84,6 @@ export class NavDragDrop { let path = ""; if (item) { let new_uploads = await this.scan_files(item, path); - console.log(new_uploads); uploads.push(... new_uploads); } else { reject(); @@ -98,10 +99,29 @@ export class NavDragDrop { * @returns {FileUpload[]} */ async handle_conflicts(uploads) { + let test_paths = []; + for (let upload of uploads) + test_paths.push(upload.path); + let proc = cockpit.spawn( + ["/usr/share/cockpit/navigator/scripts/return-exists.py3", ... test_paths], + {error: "out", superuser: "try"} + ); + let exist_result; + proc.done((data) => { + exist_result = JSON.parse(data); + }); + proc.fail((e, data) => { + this.nav_window_ref.modal_prompt.alert(e, data); + }); + try { + await proc; + } catch { + return; + } let keepers = []; let requests = {}; for (let upload of uploads) { - if (!await upload.check_if_exists()) { + if (!exist_result[upload.path]) { keepers.push(upload.filename); continue; } @@ -147,6 +167,7 @@ export class NavDragDrop { this.drop_area.classList.remove("drag-enter"); break; case "drop": + this.nav_window_ref.start_load(); let uploads; let items = e.dataTransfer.items; e.preventDefault(); @@ -162,10 +183,13 @@ export class NavDragDrop { } } this.drop_area.classList.remove("drag-enter"); - if (uploads.length === 0) + if (uploads.length === 0) { + this.nav_window_ref.stop_load(); break; + } uploads = await this.handle_conflicts(uploads); - uploads.forEach((upload) => {upload.upload()}); + this.nav_window_ref.stop_load(); + this.upload_manager.add(... uploads); break; default: this.drop_area.classList.remove("drag-enter"); diff --git a/navigator/components/NavEntry.js b/navigator/components/NavEntry.js index bd3fd03..2d97663 100644 --- a/navigator/components/NavEntry.js +++ b/navigator/components/NavEntry.js @@ -18,7 +18,7 @@ */ import {NavWindow} from "./NavWindow.js"; -import {format_bytes, property_entry_html, format_time} from "../functions.js"; +import {format_bytes, property_entry_html, format_time, check_if_exists} from "../functions.js"; export class NavEntry { /** @@ -33,13 +33,14 @@ export class NavEntry { this.path = path.split("/").splice(1); else this.path = (path.length) ? path : [""]; + this.filename = this.get_filename(); this.dom_element = document.createElement("div"); this.dom_element.classList.add("nav-item"); let icon = this.dom_element.nav_item_icon = document.createElement("i"); icon.classList.add("nav-item-icon"); let title = this.dom_element.nav_item_title = document.createElement("div"); title.classList.add("nav-item-title", "no-select"); - title.innerText = this.filename(); + title.innerText = this.filename; this.dom_element.appendChild(icon); this.dom_element.appendChild(title); let title_edit = this.dom_element.nav_item_title.editor = document.createElement("input"); @@ -63,10 +64,10 @@ export class NavEntry { this.dom_element.addEventListener("click", this); this.dom_element.addEventListener("contextmenu", this); } - this.is_hidden_file = this.filename().startsWith('.'); + this.is_hidden_file = this.filename.startsWith('.'); if (this.is_hidden_file) icon.style.opacity = 0.5; - this.dom_element.title = this.filename(); + this.dom_element.title = this.filename; if (nav_window_ref && nav_window_ref.item_display === "list") { let mode = document.createElement("div"); let owner = document.createElement("div"); @@ -85,6 +86,7 @@ export class NavEntry { this.dom_element.appendChild(group); this.dom_element.appendChild(size); } + this.visible = true; } /** @@ -97,13 +99,20 @@ export class NavEntry { if (this.nav_window_ref.selected_entries.size === 1 && this.nav_window_ref.selected_entries.has(this)) { switch (e.target) { case this.dom_element.nav_item_title: - this.show_edit(e.target); + this.double_click = true; + if(this.timeout) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.double_click = false; + if (!this.is_dangerous_path()) + this.show_edit(e.target); + }, 500); e.stopPropagation(); break; default: break; } - } + } this.nav_window_ref.set_selected(this, e.shiftKey, e.ctrlKey); this.context_menu_ref.hide(); break; @@ -127,7 +136,7 @@ export class NavEntry { * * @returns {string} */ - filename() { + get_filename() { var name = this.path[this.path.length - 1]; if (!name) name = "/"; @@ -151,10 +160,12 @@ export class NavEntry { } show() { + this.visible = true; this.dom_element.style.display = "flex"; } hide() { + this.visible = false; this.dom_element.style.display = "none"; } @@ -255,10 +266,13 @@ export class NavEntry { * @param {string} new_path */ async rename(new_name) { - if (new_name === this.filename()) + if (new_name === this.filename) return; if (new_name.includes("/")) { - this.nav_window_ref.modal_prompt.alert("File name can't contain `/`."); + this.nav_window_ref.modal_prompt.alert( + "File name can't contain `/`.", + "If you want to move the file, right click > cut then right click > paste." + ); return; } else if (new_name === "..") { this.nav_window_ref.modal_prompt.alert( @@ -267,6 +281,14 @@ export class NavEntry { ); return; } + let new_path = [this.nav_window_ref.pwd().path_str(), new_name].join("/"); + if (await check_if_exists(new_path)) { + this.nav_window_ref.modal_prompt.alert( + "Failed to rename.", + "File exists: " + new_path + ); + return; + } try { await this.mv(new_name); } catch(e) { @@ -285,11 +307,22 @@ export class NavEntry { if (!element.editor) return; element.hide_func = () => {this.hide_edit(element)}; - element.editor.onchange = element.hide_func; + element.keydown_handler = (e) => { + switch (e.keyCode) { + case 13: // enter + this.apply_edit(element); + case 27: // esc + this.hide_edit(element); + break; + default: + break; + } + }; + element.editor.addEventListener("keydown", element.keydown_handler); window.addEventListener("click", element.hide_func); switch (element) { case this.dom_element.nav_item_title: - element.editor.value = this.filename(); + element.editor.value = this.filename; break; default: element.editor.value = element.innerText; @@ -301,7 +334,7 @@ export class NavEntry { element.editor.focus(); } - hide_edit(element) { + apply_edit(element) { if (!element.editor) return; switch (element) { @@ -311,8 +344,14 @@ export class NavEntry { default: break; } + } + + hide_edit(element) { + if (!element.editor) + return; element.editor.style.display = "none"; element.style.display = "inline-block"; + element.editor.removeEventListener("keydown", element.keydown_handler) window.removeEventListener("click", element.hide_func); } @@ -323,8 +362,8 @@ export class NavEntry { show_properties(extra_properties = "") { var selected_name_fields = document.getElementsByClassName("nav-info-column-filename"); for (let elem of selected_name_fields) { - elem.innerHTML = this.filename(); - elem.title = this.filename(); + elem.innerHTML = this.filename; + elem.title = this.filename; } var html = ""; html += property_entry_html("Mode", this.stat["mode-str"]); @@ -339,7 +378,7 @@ export class NavEntry { } populate_edit_fields() { - document.getElementById("nav-edit-filename").innerText = this.filename(); + document.getElementById("nav-edit-filename").innerText = this.filename; var mode_bits = [ "other-exec", "other-write", "other-read", "group-exec", "group-write", "group-read", @@ -353,4 +392,20 @@ export class NavEntry { document.getElementById("nav-edit-owner").value = this.stat["owner"]; document.getElementById("nav-edit-group").value = this.stat["group"]; } + + style_selected() { + this.dom_element.classList.add("nav-item-selected"); + } + + unstyle_selected() { + this.dom_element.classList.remove("nav-item-selected"); + } + + /** + * + * @returns {boolean} + */ + is_dangerous_path() { + return this.nav_window_ref.dangerous_dirs.includes(this.path_str()); + } } diff --git a/navigator/components/NavFile.js b/navigator/components/NavFile.js index e06fd30..6fd717f 100644 --- a/navigator/components/NavFile.js +++ b/navigator/components/NavFile.js @@ -44,6 +44,9 @@ export class NavFile extends NavEntry { switch(e.type){ case "click": if (this.double_click) { + if(this.timeout) + clearTimeout(this.timeout); + this.double_click = false; this.open(); return; } else { // single click @@ -94,7 +97,7 @@ export class NavFile extends NavEntry { this.show_edit_file_contents(); } else { console.log("Unknown mimetype: " + type); - if (await this.nav_window_ref.modal_prompt.confirm("Can't open " + this.filename() + " for editing.", "Download it instead?")) { + if (await this.nav_window_ref.modal_prompt.confirm("Can't open " + this.filename + " for editing.", "Download it instead?")) { var download = new NavDownloader(this); download.download(); } @@ -118,7 +121,7 @@ export class NavFile extends NavEntry { document.getElementById("nav-cancel-edit-contents-btn").onclick = this.hide_edit_file_contents.bind(this); document.getElementById("nav-continue-edit-contents-btn").onclick = this.write_to_file.bind(this); document.getElementById("nav-edit-contents-header").innerText = "Editing " + this.path_str(); - document.getElementById("nav-contents-view").style.display = "none"; + document.getElementById("nav-contents-view-holder").style.display = "none"; document.getElementById("nav-edit-contents-view").style.display = "flex"; } @@ -140,7 +143,7 @@ export class NavFile extends NavEntry { window.addEventListener("keydown", this.nav_window_ref); document.getElementById("nav-edit-contents-textarea").removeEventListener("keydown", this); document.getElementById("nav-edit-contents-view").style.display = "none"; - document.getElementById("nav-contents-view").style.display = "flex"; + document.getElementById("nav-contents-view-holder").style.display = "flex"; this.nav_window_ref.enable_buttons(); } } @@ -195,7 +198,7 @@ export class NavFileLink extends NavFile{ this.show_edit_file_contents(); } else { console.log("Unknown mimetype: " + type); - this.nav_window_ref.modal_prompt.alert("Can't open " + this.filename() + " for editing."); + this.nav_window_ref.modal_prompt.alert("Can't open " + this.filename + " for editing."); } } @@ -218,7 +221,7 @@ export class NavFileLink extends NavFile{ document.getElementById("nav-cancel-edit-contents-btn").onclick = this.hide_edit_file_contents.bind(this); document.getElementById("nav-continue-edit-contents-btn").onclick = this.write_to_file.bind(this); document.getElementById("nav-edit-contents-header").innerHTML = "Editing " + this.path_str() + ' ' + this.get_link_target_path(); - document.getElementById("nav-contents-view").style.display = "none"; + document.getElementById("nav-contents-view-holder").style.display = "none"; document.getElementById("nav-edit-contents-view").style.display = "flex"; } diff --git a/navigator/components/NavWindow.js b/navigator/components/NavWindow.js index 5967f54..063682e 100644 --- a/navigator/components/NavWindow.js +++ b/navigator/components/NavWindow.js @@ -38,7 +38,6 @@ export class NavWindow { this.window.addEventListener("click", this); this.window.addEventListener("contextmenu", this); window.addEventListener("keydown", this); - this.last_selected_index = -1; this.context_menu = new NavContextMenu("nav-context-menu", this); @@ -49,6 +48,36 @@ export class NavWindow { this.sort_function = new SortFunctions(); this.modal_prompt = new ModalPrompt(); + + this.dangerous_dirs = [ + "/", + "/bin", + "/boot", + "/dev", + "/etc", + "/home", + "/lib", + "/lib32", + "/lib64", + "/mnt", + "/opt", + "/proc", + "/root", + "/run", + "/sbin", + "/srv", + "/sys", + "/tmp", + "/usr", + "/usr/bin", + "/usr/include", + "/usr/lib", + "/usr/lib32", + "/usr/lib64", + "/usr/sbin", + "/usr/share", + "/var" + ]; } /** @@ -59,7 +88,7 @@ export class NavWindow { switch (e.type) { case "click": if (e.target === this.window) { - this.clear_selected(); + this.reset_selection(); this.show_selected_properties(); } break; @@ -73,6 +102,8 @@ export class NavWindow { } else if (e.keyCode === 65 && e.ctrlKey) { this.select_all(); e.preventDefault(); + } else if (e.keyCode === 27) { + this.reset_selection(); } else if (e.keyCode === 67 && e.ctrlKey) { this.copy(); } else if (e.keyCode === 86 && e.ctrlKey) { @@ -129,7 +160,7 @@ export class NavWindow { file.context_menu_ref = this.context_menu; }); document.getElementById("pwd").value = this.pwd().path_str(); - this.set_selected(this.pwd(), false, false); + this.reset_selection(); this.show_selected_properties(); document.getElementById("nav-num-dirs").innerText = `${num_dirs} Director${(num_dirs === 1)? "y" : "ies"}`; document.getElementById("nav-num-files").innerText = `${num_files} File${(num_files === 1)? "" : "s"}`; @@ -178,65 +209,85 @@ export class NavWindow { this.cd(new NavDir(this.pwd().parent_dir())); } + clear_selected() { + for (let entry of this.selected_entries) { + entry.unstyle_selected(); + } + this.selected_entries.clear(); + } + /** * * @param {NavEntry} entry - * @param {Boolean} select_range - * @param {Boolean} append */ - set_selected(entry, select_range, append) { - this.hide_edit_selected(); - for (let i of this.selected_entries) { - i.dom_element.classList.remove("nav-item-selected"); - if (i.nav_type === "dir") { - i.dom_element.nav_item_icon.classList.remove("fa-folder-open"); - i.dom_element.nav_item_icon.classList.add("fa-folder"); - } - } - var to_be_selected = []; - if (append && this.selected_entries.has(entry)) { - this.selected_entries.delete(entry); - if (this.selected_entries.size === 0) { - this.clear_selected(); - } - } else if (select_range && this.last_selected_index !== -1) { - var start = this.last_selected_index; - var end = this.entries.indexOf(entry); - if (end < start) - [start, end] = [end, start]; - if (end === -1) - return; - to_be_selected = this.entries.slice(start, end + 1).filter(entry => !entry.stat["inaccessible"]); - } else { - if (!append) - this.selected_entries.clear(); - to_be_selected = [entry]; - } - for (let i of to_be_selected) { - this.selected_entries.add(i); - } - for (let i of this.selected_entries) { - i.dom_element.classList.add("nav-item-selected"); - if (i.nav_type === "dir") { - i.dom_element.nav_item_icon.classList.remove("fa-folder"); - i.dom_element.nav_item_icon.classList.add("fa-folder-open"); - } - } - if (this.selected_entries.size > 1){ - var name_fields = document.getElementsByClassName("nav-info-column-filename"); - for (let name_field of name_fields) { - name_field.innerText = this.selected_entries.size.toString() + " selected" - name_field.title = name_field.innerText; - } - document.getElementById("nav-info-column-properties").innerHTML = ""; - } else { - this.show_selected_properties(); - } - this.last_selected_index = this.entries.indexOf(entry); + select_one(entry) { + entry.style_selected(); + this.selected_entries.add(entry); } - - clear_selected() { - this.set_selected(this.pwd(), false, false); + + deselect_one(entry) { + entry.unstyle_selected(); + this.selected_entries.delete(entry); + } + + /** + * + * @param {NavEntry} start + * @param {NavEntry} end + */ + select_range(start, end) { + let start_ind = this.entries.indexOf(start); + let end_ind = this.entries.indexOf(end); + if (start_ind === -1 || end_ind === -1) + return; + if (end_ind === start_ind) + this.select_one(start); + else if (end_ind < start_ind) + [start_ind, end_ind] = [end_ind, start_ind]; + for (let i = start_ind; i <= end_ind; i++) { + let entry = this.entries[i]; + if (entry.visible && (!entry.is_hidden_file || this.show_hidden)) + this.select_one(entry); + } + } + + reset_selection() { + this.clear_selected(); + this.select_one(this.pwd()); + this.last_selected_entry = null; + this.update_selection_info(); + } + + /** + * + * @param {NavEntry} target + * @param {Boolean} shift + * @param {Boolean} ctrl + */ + set_selected(target, shift, ctrl) { + if (!ctrl && !shift) + this.clear_selected(); + if (!shift || !this.last_selected_entry) + this.last_selected_entry = target; + if (shift) { + if (!ctrl) + this.clear_selected(); + this.select_range( + this.last_selected_entry ?? this.entries[0], + target + ); + } else if (ctrl && this.selected_entries.has(target)) { + this.deselect_one(target) + } else { + this.select_one(target); + } + this.update_selection_info(); + } + + select_all() { + this.clear_selected(); + this.select_range(this.entries[0], this.entries[this.entries.length - 1]); + this.update_selection_info(); } selected_entry() { @@ -248,29 +299,27 @@ export class NavWindow { } show_selected_properties() { - this.selected_entry().show_properties(); + this.selected_entry()?.show_properties?.(); + } + + update_selection_info() { + if (this.selected_entries.size > 1){ + var name_fields = document.getElementsByClassName("nav-info-column-filename"); + for (let name_field of name_fields) { + name_field.innerText = this.selected_entries.size.toString() + " selected" + name_field.title = name_field.innerText; + } + document.getElementById("nav-info-column-properties").innerHTML = ""; + } else { + this.show_selected_properties(); + } } async show_edit_selected() { - var dangerous_dirs = [ - "/", - "/usr", - "/bin", - "/sbin", - "/lib", - "/lib32", - "/lib64", - "/usr/bin", - "/usr/include", - "/usr/lib", - "/usr/lib32", - "/usr/lib64", - "/usr/sbin", - ]; var dangerous_selected = []; for (let i of this.selected_entries) { var path = i.path_str(); - if (dangerous_dirs.includes(path)) { + if (this.dangerous_dirs.includes(path)) { dangerous_selected.push(path); } } @@ -321,7 +370,7 @@ export class NavWindow { } var targets = []; for (let target of this.selected_entries) { - targets.push(target.filename()); + targets.push(target.filename); } var targets_str = targets.join(", "); document.getElementById("selected-files-list-header").innerText = "Applying edits to:"; @@ -395,6 +444,8 @@ export class NavWindow { } async delete_selected() { + if (await this.check_if_dangerous("delete")) + return; var prompt = ""; if (this.selected_entries.size > 1) { prompt = "Deleting " + this.selected_entries.size + " files."; @@ -404,6 +455,7 @@ export class NavWindow { if (!await this.modal_prompt.confirm(prompt, "This cannot be undone. Are you sure?", true)) { return; } + this.start_load(); for (let target of this.selected_entries) { try { await target.rm(); @@ -411,6 +463,7 @@ export class NavWindow { this.modal_prompt.alert(e); } } + this.stop_load(); this.refresh(); } @@ -545,7 +598,9 @@ export class NavWindow { this.refresh(); } - cut() { + async cut() { + if (await this.check_if_dangerous("move")) + return; this.clip_board = [...this.selected_entries]; this.copy_or_move = "move"; this.paste_cwd = this.pwd().path_str(); @@ -643,6 +698,7 @@ export class NavWindow { default: break; } + e.stopPropagation(); } nav_bar_cd() { @@ -681,7 +737,7 @@ export class NavWindow { start_load() { document.getElementById("nav-loader-container").style.display = "block"; - var buttons = document.getElementsByTagName("button"); + var buttons = document.getElementsByClassName("disable-while-loading"); for (let button of buttons) { button.disabled = true; } @@ -689,7 +745,7 @@ export class NavWindow { stop_load() { document.getElementById("nav-loader-container").style.display = "none"; - var buttons = document.getElementsByTagName("button"); + var buttons = document.getElementsByClassName("disable-while-loading"); for (let button of buttons) { button.disabled = false; } @@ -801,15 +857,6 @@ export class NavWindow { this.set_nav_button_state(); } - select_all() { - this.selected_entries.clear(); - for (let entry of this.entries) { - if (!entry.is_hidden_file || this.show_hidden) { - this.set_selected(entry, false, true); - } - } - } - async switch_item_display() { var button = document.getElementById("nav-item-display-icon"); if (this.item_display === "grid") { @@ -833,11 +880,42 @@ export class NavWindow { search_filter(event) { var search_name = event.target.value; + let search_func; + if (search_name[0] === '*') + search_func = (entry) => entry.filename.toLowerCase().includes(search_name.slice(1).toLowerCase()); + else + search_func = (entry) => entry.filename.toLowerCase().startsWith(search_name.toLowerCase()); this.entries.forEach((entry) => { - if (entry.filename().toLowerCase().startsWith(search_name.toLowerCase())) + if (search_func(entry)) entry.show(); else entry.hide(); }); } + + /** + * + * @param {string} verb + * @returns {Promise} + */ + check_if_dangerous(verb) { + return new Promise(async (resolve, reject) => { + let dangerous_selected = []; + for (let entry of this.selected_entries) { + let path = entry.path_str(); + if (this.dangerous_dirs.includes(path)) { + dangerous_selected.push(path); + } + } + if (dangerous_selected.length) { + await this.modal_prompt.alert( + `Cannot ${verb} system-critical paths.`, + `The following path(s) are very dangerous to ${verb}: ${dangerous_selected.join(", ")}. If you think you need to ${verb} them, use the terminal.` + ); + resolve(true); + } else { + resolve(false); + } + }); + } } diff --git a/navigator/components/SortFunctions.js b/navigator/components/SortFunctions.js index bb7845b..e6f322d 100644 --- a/navigator/components/SortFunctions.js +++ b/navigator/components/SortFunctions.js @@ -60,11 +60,11 @@ export class SortFunctions { } name_asc(first, second) { - return first.filename().localeCompare(second.filename()); + return first.filename.localeCompare(second.filename); } name_desc(first, second) { - return second.filename().localeCompare(first.filename()); + return second.filename.localeCompare(first.filename); } owner_asc(first, second) { diff --git a/navigator/functions.js b/navigator/functions.js index cbada10..0dc0853 100644 --- a/navigator/functions.js +++ b/navigator/functions.js @@ -70,9 +70,7 @@ export function format_time_remaining(seconds_) { if (hours) { out = String(hours).padStart(2, '0') + ":"; } - if (minutes) { - out += String(minutes).padStart(2, '0') + ":"; - } + out += String(minutes).padStart(2, '0') + ":"; out += String(seconds).padStart(2, '0'); return out; } @@ -95,4 +93,17 @@ export function format_permissions(mode) { } } return result; -} \ No newline at end of file +} + +/** + * + * @param {string} path + * @returns {Promise} + */ +export function check_if_exists(path) { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/fail-if-exists.py3", path], {superuser: "try"}); + proc.done((data) => {resolve(false)}); + proc.fail((e, data) => {resolve(true)}); + }); +} diff --git a/navigator/navigator.css b/navigator/navigator.css index fd861d1..5e0be1e 100644 --- a/navigator/navigator.css +++ b/navigator/navigator.css @@ -651,7 +651,7 @@ input:checked + .slider:before { .nav-notifications { position: absolute; bottom: 0; - right: 0; + right: 10px; padding: 5px; display: flex; flex-flow: column-reverse nowrap; @@ -663,7 +663,7 @@ input:checked + .slider:before { .nav-notification { margin: 5px; position: relative; - display: flex; + display: none; flex-flow: column nowrap; align-items: stretch; z-index: 10; @@ -673,15 +673,23 @@ input:checked + .slider:before { border-radius: var(--nav-border-radius); color: var(--font); } - -.nav-notification-header { +/* .nav-notification-header { position: relative; z-index: 10; font-weight: bold; -} +} */ -.nav-notification-header > progress { +/* .nav-notification-header > progress { position: relative; z-index: 10; +} */ + +.nav-notification-header > p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } +.no-border { + border: none; +} \ No newline at end of file diff --git a/navigator/navigator.html b/navigator/navigator.html index 72df661..9ad6bc2 100644 --- a/navigator/navigator.html +++ b/navigator/navigator.html @@ -40,13 +40,13 @@
- diff --git a/navigator/navigator.js b/navigator/navigator.js index f920452..20dfdb4 100644 --- a/navigator/navigator.js +++ b/navigator/navigator.js @@ -130,6 +130,7 @@ function set_up_buttons() { document.getElementById("search-bar").addEventListener("keydown", (e) => { if (e.keyCode === 13) nav_window.search_filter(e); + e.stopPropagation(); }); // fix tab in editor input document.getElementById('nav-edit-contents-textarea').addEventListener('keydown', (e) => { diff --git a/navigator/scripts/return-exists.py3 b/navigator/scripts/return-exists.py3 new file mode 100755 index 0000000..38ce962 --- /dev/null +++ b/navigator/scripts/return-exists.py3 @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +""" + Cockpit Navigator - A File System Browser for Cockpit. + Copyright (C) 2021 Josh Boudreau + + This file is part of Cockpit Navigator. + Cockpit Navigator 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. + Cockpit Navigator 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 Cockpit Navigator. If not, see . +""" + +""" +Synopsys: return-exists.py3 /full/path1 [/full/path2 ...] +replys with JSON formatted dictionary of path : boolean where true means the file exists +""" + +import os +import sys +import json + +def main(): + argv = sys.argv + argc = len(sys.argv) + if argc <= 1: + print("No arguments provided") + sys.exit(1) + response = {} + for i in range (1, argc): + path = argv[i] + response[path] = os.path.lexists(path) + print(json.dumps(response)) + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/navigator/scripts/write-chunks.py3 b/navigator/scripts/write-chunks.py3 index 3ccb8df..6230a82 100755 --- a/navigator/scripts/write-chunks.py3 +++ b/navigator/scripts/write-chunks.py3 @@ -18,7 +18,7 @@ """ """ -Synopsis: `write-chunks.py3 ` +Synopsis: `echo | write-chunks.py3` JSON objects are of form: obj = { seek: @@ -35,20 +35,24 @@ def write_chunk(chunk, file): if not file: path = sys.argv[1] parent_path = os.path.dirname(path) - if not os.path.exists(parent_path): - os.makedirs(parent_path, exist_ok=True) - elif os.path.isfile(parent_path): - print(parent_path + ": exists and is not a directory.") - sys.exit(1) try: + if not os.path.exists(parent_path): + os.makedirs(parent_path, exist_ok=True) + elif os.path.isfile(parent_path): + print(parent_path + ": exists and is not a directory.") + sys.exit(1) file = open(path, "wb") except Exception as e: print(e) sys.exit(1) seek = chunk["seek"] data = base64.b64decode(chunk["chunk"]) - file.seek(seek) - file.write(data) + try: + file.seek(seek) + file.write(data) + except Exception as e: + print(e) + sys.exit(1) def main(): file = None diff --git a/packaging/el7/main.spec b/packaging/el7/main.spec index b71cc21..b6d2012 100644 --- a/packaging/el7/main.spec +++ b/packaging/el7/main.spec @@ -32,6 +32,11 @@ rm -rf %{buildroot} /usr/share/cockpit/navigator/* %changelog +* Tue Jul 20 2021 Josh Boudreau 0.5.4-1 +- Add fuzzy search. +- Optimize folder uploads. +- Fix bugs with selecting entries and renaming files. +- Stop user from deleting or renaming system-critical paths. * Mon Jul 19 2021 Josh Boudreau 0.5.3-1 - Implement inline filename editing. - Add information popup button. diff --git a/packaging/el8/main.spec b/packaging/el8/main.spec index b71cc21..b6d2012 100644 --- a/packaging/el8/main.spec +++ b/packaging/el8/main.spec @@ -32,6 +32,11 @@ rm -rf %{buildroot} /usr/share/cockpit/navigator/* %changelog +* Tue Jul 20 2021 Josh Boudreau 0.5.4-1 +- Add fuzzy search. +- Optimize folder uploads. +- Fix bugs with selecting entries and renaming files. +- Stop user from deleting or renaming system-critical paths. * Mon Jul 19 2021 Josh Boudreau 0.5.3-1 - Implement inline filename editing. - Add information popup button. diff --git a/packaging/focal/changelog b/packaging/focal/changelog index baee9c9..96fd20f 100644 --- a/packaging/focal/changelog +++ b/packaging/focal/changelog @@ -1,3 +1,12 @@ +cockpit-navigator (0.5.4-1focal) focal; urgency=medium + + * Add fuzzy search. + * Optimize folder uploads. + * Fix bugs with selecting entries and renaming files. + * Stop user from deleting or renaming system-critical paths. + + -- Josh Boudreau Tue, 20 Jul 2021 13:06:32 -0300 + cockpit-navigator (0.5.3-1focal) focal; urgency=medium * Implement inline filename editing.