diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 70b71f0..9beadf0 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -30,6 +30,7 @@ jobs: files: | ${{github.workspace}}/dist/packages/*/*.deb ${{github.workspace}}/dist/packages/*/*.rpm + ${{github.workspace}}/LICENSE env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Update Repository diff --git a/CHANGELOG.md b/CHANGELOG.md index ebaef3e..57bf448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## Cockpit Navigator 0.4.6-3 +## Cockpit Navigator 0.5.0-1 -* Add relase for el7 \ No newline at end of file +* Implement custom modal style popups to replace browser dialogs. \ No newline at end of file diff --git a/README.md b/README.md index 7499601..ffe9d22 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.4/cockpit-navigator_0.4.6-3focal_all.deb` -1. `# apt install ./cockpit-navigator_0.4.6-3focal_all.deb` +1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.0/cockpit-navigator_0.5.0-1focal_all.deb` +1. `# apt install ./cockpit-navigator_0.5.0-1focal_all.deb` ### EL7 -1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.4/cockpit-navigator-0.4.6-3.el7.noarch.rpm` +1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.0/cockpit-navigator-0.5.0-1.el7.noarch.rpm` ### EL8 -1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.4/cockpit-navigator-0.4.6-3.el8.noarch.rpm` +1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.0/cockpit-navigator-0.5.0-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.4.6 is latest) +1. `$ git checkout ` (v0.5.0 is latest) 1. `# make install` ## From 45Drives Repositories ### Ubuntu 1. Import GPG Key ```sh -wget -qO - http://images.45drives.com/repo/keys/aptpubkey.asc | sudo apt-key add - +wget -qO - http://repo.45drives.com/key.asc | sudo apt-key add - ``` 2. Add 45drives.list ```sh cd /etc/apt/sources.list.d -sudo wget http://images.45drives.com/repo/debian/45drives.list +sudo wget http://repo.45drives.com/debian/45drives.list sudo apt update ``` 3. Install Package @@ -55,7 +55,7 @@ sudo apt install cockpit-navigator 1. Add Repository ```sh cd /etc/yum.repos.d -sudo wget http://images.45drives.com/repo/centos/45drives-centos.repo +sudo wget http://repo.45drives.com/rhel/45drives.repo sudo yum clean all ``` 2. Install Package diff --git a/makefile b/makefile index 856ce18..cea61d6 100644 --- a/makefile +++ b/makefile @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with Cockpit Navigator. If not, see . +EL7_DIST=.el7 + default: @@ -21,8 +23,9 @@ all: default install: mkdir -p $(DESTDIR)/usr/share/cockpit/ cp -rpf navigator $(DESTDIR)/usr/share/cockpit -ifeq ($(DIST),"el7") +ifeq ($(DIST),$(EL7_DIST)) sed -i "s/pf-c-button/btn/g;s/pf-m-primary/btn-primary/g;s/pf-m-secondary/btn-default/g;s/pf-m-danger/btn-danger/g" $(DESTDIR)/usr/share/cockpit/navigator/navigator.html + sed -i "s/pf-c-button/btn/g;s/pf-m-primary/btn-primary/g;s/pf-m-secondary/btn-default/g;s/pf-m-danger/btn-danger/g" $(DESTDIR)/usr/share/cockpit/navigator/components/ModalPrompt.js endif uninstall: @@ -31,7 +34,7 @@ uninstall: install-local: mkdir -p $(HOME)/.local/share/cockpit cp -rpf navigator $(HOME)/.local/share/cockpit - sed -i "s#\"/usr/share/\(cockpit/navigator/scripts/.*\)\"#\"$(HOME)/.local/share/\1\"#g" $(HOME)/.local/share/cockpit/navigator/navigator.js + find $(HOME)/.local/share/cockpit/navigator -name '*.js' -exec sed -i "s#\"/usr/share/\(cockpit/navigator/scripts/.*\)\"#\"$(HOME)/.local/share/\1\"#g" {} \; uninstall-local: rm -rf $(HOME)/.local/share/cockpit/navigator diff --git a/manifest.json b/manifest.json index 5731e81..a26889b 100644 --- a/manifest.json +++ b/manifest.json @@ -3,8 +3,8 @@ "name": "cockpit-navigator", "title": "Cockpit Navigator", "prerelease": false, - "version": "0.4.6", - "buildVersion": "3", + "version": "0.5.0", + "buildVersion": "1", "author": "Josh Boudreau ", "url": "https://github.com/45Drives/cockpit-navigator", "category": "utils", @@ -54,8 +54,8 @@ ], "changelog": { "urgency": "medium", - "version": "0.4.6", - "buildVersion": "3", + "version": "0.5.0", + "buildVersion": "1", "ignore": [], "date": null, "packager": "Josh Boudreau ", diff --git a/navigator/components/FileUpload.js b/navigator/components/FileUpload.js new file mode 100644 index 0000000..f65139b --- /dev/null +++ b/navigator/components/FileUpload.js @@ -0,0 +1,185 @@ +/* + 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 {NavWindow} from "./NavWindow.js"; +import {format_time_remaining} from "../functions.js"; + +export class FileUpload { + /** + * + * @param {File|Blob} file + * @param {NavWindow} nav_window_ref + */ + constructor(file, nav_window_ref) { + try { + this.chunk_size = (parseInt(cockpit.info.version) > 238)? 1048576 : 65536; + } catch(e) { + console.log(e); + this.chunk_size = 65536; + } + this.filename = file.name; + this.nav_window_ref = nav_window_ref; + this.path = nav_window_ref.pwd().path_str() + "/" + file.name; + this.reader = new FileReader(); + this.chunks = this.slice_file(file); + this.chunk_index = 0; + this.timestamp = Date.now(); + } + + 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)}); + }); + } + + make_html_element() { + var notification = 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; + + var info = document.createElement("div"); + info.classList.add("flex-row", "space-between"); + notification.appendChild(info); + + var rate = document.createElement("div"); + rate.classList.add("monospace-sm"); + info.appendChild(rate); + rate.innerText = "-"; + this.rate = rate; + + var eta = document.createElement("div"); + eta.classList.add("monospace-sm"); + info.appendChild(eta); + eta.innerText = "-"; + this.eta = eta; + + var progress = document.createElement("progress"); + progress.max = this.num_chunks; + notification.appendChild(progress); + this.progress = progress; + + this.html_elements = [progress, eta, rate, info, header, notification]; + document.getElementById("nav-notifications").appendChild(notification); + } + + remove_html_element() { + for (let elem of this.html_elements) { + if (elem.parentElement) + elem.parentElement.removeChild(elem); + } + } + + /** + * + * @param {File|Blob} file + * @returns {Array} + */ + slice_file(file) { + var offset = 0; + var next_offset; + var chunks = []; + this.num_chunks = Math.ceil(file.size / this.chunk_size); + for (let i = 0; i < this.num_chunks; i++) { + next_offset = Math.min(this.chunk_size * (i + 1), file.size); + chunks.push(file.slice(offset, next_offset)); + offset = next_offset; + } + return chunks; + } + + async upload() { + if (await this.check_if_exists()) { + if (!await this.nav_window_ref.modal_prompt.confirm(this.filename + ": File exists. Replace?", "", true)) + return; + } + this.make_html_element(); + 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 = () => {} + this.done(); + this.nav_window_ref.modal_prompt.alert(data); + }) + this.proc.done((data) => { + this.nav_window_ref.refresh(); + }) + this.reader.onload = (function(uploader_ref) { + return async function(evt) { + uploader_ref.write_to_file(evt, uploader_ref.chunk_index * uploader_ref.chunk_size); + uploader_ref.chunk_index++; + uploader_ref.progress.value = uploader_ref.chunk_index; + if (uploader_ref.chunk_index < uploader_ref.num_chunks) + uploader_ref.reader.readAsArrayBuffer(uploader_ref.chunks[uploader_ref.chunk_index]); + else { + uploader_ref.done(); + } + }; + })(this); + this.reader.readAsArrayBuffer(this.chunks[0]); + } + + arrayBufferToBase64(buffer) { + let binary = ''; + let bytes = new Uint8Array(buffer); + let len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + } + + /** + * + * @param {Event} evt + */ + write_to_file(evt) { + var chunk_b64 = this.arrayBufferToBase64(evt.target.result); + const seek = this.chunk_index * this.chunk_size; + var obj = { + seek: seek, + chunk: chunk_b64 + }; + this.proc.input(JSON.stringify(obj) + "\n", true); + this.update_xfr_rate(); + } + + done() { + this.proc.input(); // close stdin + this.remove_html_element(); + } + + update_xfr_rate() { + var now = Date.now(); + var elapsed = (now - this.timestamp) / 1000; + this.timestamp = now; + var rate = this.chunk_size / elapsed; + this.rate.innerText = cockpit.format_bytes_per_sec(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); + } +} diff --git a/navigator/components/ModalPrompt.js b/navigator/components/ModalPrompt.js new file mode 100644 index 0000000..bfebdc5 --- /dev/null +++ b/navigator/components/ModalPrompt.js @@ -0,0 +1,230 @@ +/* + 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 . + */ + +/** + * @typedef {Object} Request + * @property {string} label + * @property {"text"|"checkbox"} type + * @property {string|undefined} default + */ + +let primary_btn = "pf-m-primary"; +let secondary_btn = "pf-m-secondary"; +let danger_btn = "pf-m-danger"; +let all_btn = [primary_btn, secondary_btn, danger_btn]; + +export class ModalPrompt { + constructor() { + this.ok = document.createElement("button"); + this.ok.innerText = "OK"; + this.ok.classList.add("pf-c-button", "pf-m-primary"); + this.cancel = document.createElement("button"); + this.cancel.innerText = "Cancel"; + this.cancel.classList.add("pf-c-button", "pf-m-secondary"); + this.yes = document.createElement("button"); + this.yes.innerText = "Yes"; + this.yes.classList.add("pf-c-button", "pf-m-primary"); + this.no = document.createElement("button"); + this.no.innerText = "No"; + this.no.classList.add("pf-c-button", "pf-m-secondary"); + this.construct_element(); + } + + construct_element() { + let bg = this.modal = document.createElement("div"); + bg.classList.add("modal"); + let fg = document.createElement("div"); + fg.classList.add("modal-dialog"); + bg.appendChild(fg); + let popup = document.createElement("div"); + popup.classList.add("modal-content"); + fg.appendChild(popup); + let header = document.createElement("div"); + header.classList.add("modal-header"); + popup.appendChild(header); + let header_text = this.header = document.createElement("h4"); + header_text.classList.add("modal-title"); + header.appendChild(header_text); + let body = this.body = document.createElement("div"); + body.classList.add("modal-body"); + popup.appendChild(body); + let footer = this.footer = document.createElement("div"); + footer.classList.add("modal-footer"); + footer.style.display = "flex"; + footer.style.flexFlow = "row no-wrap"; + footer.style.justifyContent = "flex-end"; + popup.appendChild(footer); + document.body.appendChild(this.modal); + } + + show() { + this.modal.style.display = "block"; + } + + hide() { + this.modal.style.display = "none"; + } + + /** + * + * @param {string} header + */ + set_header(header) { + this.header.innerText = header; + } + + /** + * + * @param {string} message + */ + set_body(message) { + this.body.innerHTML = ""; + this.body.innerText = message; + } + + /** + * + * @param {string} header + * @param {string} message + * @returns {Promise} + */ + alert(header, message = "") { + this.set_header(header); + this.set_body(message); + this.footer.innerHTML = ""; + this.footer.appendChild(this.ok); + this.show(); + return new Promise((resolve, reject) => { + this.ok.onclick = () => { + resolve(); + this.hide(); + } + }); + } + + /** + * + * @param {string} header + * @param {string} message + * @param {boolean} danger + * @returns {Promise} + */ + confirm(header, message = "", danger = false) { + this.set_header(header); + this.set_body(message); + this.footer.innerHTML = ""; + this.footer.append(this.no, this.yes); + this.yes.classList.remove(... all_btn); + if (danger) + this.yes.classList.add(danger_btn); + else + this.yes.classList.add(primary_btn); + this.show(); + return new Promise((resolve, reject) => { + let resolve_true = () => { + resolve(true); + this.hide(); + } + let resolve_false = () => { + resolve(false); + this.hide(); + } + this.yes.onclick = resolve_true; + this.no.onclick = resolve_false; + }); + } + + /** + * + * @param {string} header + * @param {Object.} requests + * @returns {Promise} + */ + prompt(header, requests) { + this.set_header(header); + this.body.innerHTML = ""; + this.footer.innerHTML = ""; + this.footer.append(this.cancel, this.ok); + let inputs = []; + + if (typeof requests === "object") { + let req_holder = document.createElement("div"); + req_holder.style.display = "flex"; + req_holder.style.flexFlow = "column nowrap"; + req_holder.style.alignItems = "stretch"; + this.body.appendChild(req_holder); + for(let key of Object.keys(requests)) { + let row = document.createElement("div"); + row.style.display = "flex"; + row.style.alignItems = "baseline"; + row.style.padding = "2px"; + let request = requests[key]; + let label = document.createElement("label"); + label.innerText = request.label; + label.htmlFor = key; + label.style.marginRight = "1em"; + label.style.flexBasis = "0"; + label.style.flexGrow = "1"; + let req = document.createElement("input"); + req.id = key; + req.type = request.type; + req.style.flexBasis = "0"; + if (request.hasOwnProperty("default")) { + req.value = request.default; + } + row.append(label, req); + req_holder.appendChild(row); + inputs.push(req); + switch (request.type) { + case "text": + req.style.flexGrow = "3"; + break; + default: + break; + } + } + } + + this.show(); + inputs[0].focus(); + return new Promise((resolve, reject) => { + this.ok.onclick = () => { + let response = {}; + for (let input of inputs) { + switch (input.type) { + case "checkbox": + response[input.id] = input.checked; + break; + case "text": + default: + response[input.id] = input.value; + break; + } + } + resolve(response); + this.hide(); + } + this.cancel.onclick = () => { + resolve(null); + this.hide(); + } + }); + } +} + diff --git a/navigator/components/NavContextMenu.js b/navigator/components/NavContextMenu.js new file mode 100644 index 0000000..a34bdf9 --- /dev/null +++ b/navigator/components/NavContextMenu.js @@ -0,0 +1,215 @@ +/* + 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 {NavEntry} from "./NavEntry.js"; +import {NavFile, NavFileLink} from "./NavFile.js"; +import {NavDir, NavDirLink} from "./NavDir.js"; +import {NavDownloader} from "./NavDownloader.js"; + +export class NavContextMenu { + /** + * + * @param {string} id + */ + constructor(id, nav_window_ref) { + this.dom_element = document.getElementById(id); + this.nav_window_ref = nav_window_ref; + this.menu_options = {}; + document.documentElement.addEventListener("click", (event) => { + if (event.target !== this.dom_element) + this.hide(); + }); + + var functions = [ + ["new_dir", '
'], + ["new_file", '
'], + ["new_link", '
'], + ["cut", '
'], + ["copy", '
'], + ["paste", '
'], + ["rename", '
'], + ["delete", '
'], + ["download", '
'], + ["properties", '
'] + ]; + for (let func of functions) { + var elem = document.createElement("div"); + var name_list = func[0].split("_"); + name_list.forEach((word, index) => {name_list[index] = word.charAt(0).toUpperCase() + word.slice(1)}); + elem.innerHTML = func[1] + name_list.join(" "); + elem.addEventListener("click", (e) => {this[func[0]].bind(this).apply()}); + elem.classList.add("nav-context-menu-item") + elem.id = "nav-context-menu-" + func[0]; + this.dom_element.appendChild(elem); + this.menu_options[func[0]] = elem; + } + } + + new_dir() { + this.nav_window_ref.mkdir(); + } + + new_file() { + this.nav_window_ref.touch(); + } + + new_link() { + var default_target = ""; + if (this.nav_window_ref.selected_entries.size <= 1 && this.target !== this.nav_window_ref.pwd()) + default_target = this.target.filename(); + this.nav_window_ref.ln(default_target); + } + + cut() { + this.nav_window_ref.cut(); + } + + copy() { + this.nav_window_ref.copy(); + } + + paste() { + this.nav_window_ref.paste(); + } + + async rename() { + this.hide(); + let response = await this.nav_window_ref.modal_prompt.prompt("Renaming " + this.target.filename(), + { + new_name: { + label: "New Name: ", + type: "text", + default: this.target.filename() + } + } + ); + if (response === null) + return; + var new_name = response.new_name; + if (new_name.includes("/")) { + this.nav_window_ref.modal_prompt.alert("File name can't contain `/`."); + return; + } + try { + await this.target.mv(new_name); + } catch(e) { + this.nav_window_ref.modal_prompt.alert(e); + return; + } + this.nav_window_ref.refresh(); + } + + zip_for_download() { + return new Promise((resolve, reject) => { + var cmd = [ + "/usr/share/cockpit/navigator/scripts/zip-for-download.py3", + this.nav_window_ref.pwd().path_str() + ]; + for (let entry of this.nav_window_ref.selected_entries) { + cmd.push(entry.path_str()); + } + var proc = cockpit.spawn(cmd, {superuser: "try", err: "out"}); + proc.fail((e, data) => { + reject(JSON.parse(data)); + }); + proc.done((data) => { + resolve(JSON.parse(data)); + }); + }); + } + + async download() { + var download_target = ""; + if (this.nav_window_ref.selected_entries.size === 1 && !(this.nav_window_ref.selected_entry() instanceof NavDir)) { + download_target = this.nav_window_ref.selected_entry(); + } else { + this.nav_window_ref.start_load(); + var result; + try { + result = await this.zip_for_download(); + } catch(e) { + this.nav_window_ref.stop_load(); + this.nav_window_ref.modal_prompt.alert(e.message); + return; + } + this.nav_window_ref.stop_load(); + download_target = new NavFile(result["archive-path"], result["stat"], this.nav_window_ref); + } + var download = new NavDownloader(download_target); + download.download(); + } + + delete() { + this.nav_window_ref.delete_selected(); + } + + properties() { + this.nav_window_ref.show_edit_selected(); + } + + /** + * + * @param {Event} event + * @param {NavEntry} target + */ + show(event, target) { + if (!this.nav_window_ref.none_selected()) { + if (event.shiftKey || event.ctrlKey) + this.nav_window_ref.set_selected(target, event.shiftKey, event.ctrlKey); + } else { + this.nav_window_ref.set_selected(target, false, false); + } + for (let option of Object.keys(this.menu_options)) { + this.menu_options[option].style.display = "flex"; // show all + } + // selectively hide options based on context + if (this.nav_window_ref.none_selected()) { + this.menu_options["copy"].style.display = "none"; + this.menu_options["cut"].style.display = "none"; + this.menu_options["delete"].style.display = "none"; + this.menu_options["download"].style.display = "none"; + } + if (this.nav_window_ref.selected_entries.size > 1) { + this.menu_options["rename"].style.display = "none"; + } else { + if (target instanceof NavDirLink || target instanceof NavFileLink) + this.menu_options["download"].style.display = "none"; + } + if (!this.nav_window_ref.clip_board.length) + this.menu_options["paste"].style.display = "none"; + this.target = target; + this.dom_element.style.display = "inline"; + this.dom_element.style.left = event.clientX + "px"; + var height = this.dom_element.getBoundingClientRect().height; + var max_height = window.innerHeight; + if (event.clientY > max_height - height) { + this.dom_element.style.top = event.clientY - height + "px"; + } else { + this.dom_element.style.top = event.clientY + "px"; + } + } + + hide() { + this.dom_element.style.display = "none"; + } + + hide_paste() { + this.menu_options["paste"].style.display = "none"; + } +} diff --git a/navigator/components/NavDir.js b/navigator/components/NavDir.js new file mode 100644 index 0000000..a946120 --- /dev/null +++ b/navigator/components/NavDir.js @@ -0,0 +1,261 @@ +/* + 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 {NavEntry} from "./NavEntry.js"; +import {NavFile, NavFileLink} from "./NavFile.js"; +import {NavWindow} from "./NavWindow.js"; +import {property_entry_html} from "../functions.js"; + +export class NavDir extends NavEntry { + /** + * + * @param {string|string[]} path + * @param {object} stat + * @param {NavWindow} nav_window_ref + */ + constructor(path, stat, nav_window_ref) { + super(path, stat, nav_window_ref); + this.nav_type = "dir"; + this.dom_element.nav_item_icon.classList.add("fas", "fa-folder"); + this.double_click = false; + } + + /** + * + * @param {Event} e + */ + handleEvent(e) { + switch (e.type) { + case "click": + if (this.double_click) + this.nav_window_ref.cd(this); + else { + // single click + this.double_click = true; + if (this.timeout) + clearTimeout(this.timeout); + this.timeout = setTimeout(() => { + this.double_click = false; + }, 500); + } + break; + } + super.handleEvent(e); + } + + /** + * + * @param {NavWindow} nav_window_ref + * @returns {Promise} + */ + get_children(nav_window_ref) { + return new Promise(async (resolve, reject) => { + var children = []; + var proc = cockpit.spawn( + ["/usr/share/cockpit/navigator/scripts/ls.py3", this.path_str()], + {err:"out", superuser: "try"} + ); + proc.fail((e, data) => { + reject(data); + }); + var data; + try { + data = await proc; + } catch(e) { + reject(e); + return; + } + var response = JSON.parse(data); + this.stat = response["."]["stat"]; + var entries = response["children"]; + var filename, path, stat; + entries.forEach((entry) => { + filename = entry["filename"]; + path = (this.path.length >= 1 && this.path[0]) ? [...this.path, filename] : [filename]; + stat = entry["stat"]; + switch(stat["mode-str"].charAt(0)) { + case 'd': + children.push(new NavDir(path, stat, nav_window_ref)); + break; + case 'l': + if(entry["isdir"]) + children.push(new NavDirLink(path, stat, nav_window_ref, entry["link-target"])); + else + children.push(new NavFileLink(path, stat, nav_window_ref, entry["link-target"])); + break; + default: + children.push(new NavFile(path, stat, nav_window_ref)); + break; + } + }); + resolve(children); + }); + } + + /** + * + * @returns {Promise} + */ + rm() { + return new Promise(async (resolve, reject) => { + var proc = cockpit.spawn( + ["rmdir", this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail(async (e, data) => { + if (/^rmdir: failed to remove .*: Directory not empty\n?$/.test(data)) { + if (await this.nav_window_ref.modal_prompt.confirm("WARNING: '" + this.path_str() + "' is not empty.", "Delete recursively? This can NOT be undone.", true)) { + this.rm_recursive(resolve, reject); + } + } else { + reject(data); + } + }); + }); + } + + /** + * + * @param {Function} resolve + * @param {Function} reject + */ + rm_recursive(resolve, reject) { + var proc = cockpit.spawn( + ["rm", "-rf", this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + } + + /** + * + * @returns {Object} + */ + async cephfs_dir_stats() { + try { + var proc = await cockpit.spawn( + ["/usr/share/cockpit/navigator/scripts/cephfs-dir-stats.py3", "-j", this.path_str()], + {err: "ignore"} + ); + return JSON.parse(proc)[0]; + } catch { + return null; + } + } + + /** + * + * @param {string} extra_properties + */ + async show_properties(extra_properties = "") { + if(!this.hasOwnProperty("ceph_stats")) + this.ceph_stats = await this.cephfs_dir_stats(); + // See if a JSON object exists for folder we are currently looking at + if (this.ceph_stats !== null) { + extra_properties += + '
'; + extra_properties += property_entry_html( + "Files", + this.ceph_stats.hasOwnProperty("files") ? this.ceph_stats.files : "N/A" + ); + extra_properties += property_entry_html( + "Directories", + this.ceph_stats.hasOwnProperty("subdirs") ? this.ceph_stats.subdirs : "N/A" + ); + extra_properties += property_entry_html( + "Recursive files", + this.ceph_stats.hasOwnProperty("rfiles") ? this.ceph_stats.rfiles : "N/A" + ); + extra_properties += property_entry_html( + "Recursive directories", + this.ceph_stats.hasOwnProperty("rsubdirs") ? this.ceph_stats.rsubdirs : "N/A" + ); + extra_properties += property_entry_html( + "Total size", + this.ceph_stats.hasOwnProperty("rbytes") ? this.ceph_stats.rbytes : "N/A" + ); + extra_properties += property_entry_html( + "Layout pool", + this.ceph_stats.hasOwnProperty("layout.pool") ? this.ceph_stats["layout.pool"] : "N/A" + ); + extra_properties += property_entry_html( + "Max files", + this.ceph_stats.hasOwnProperty("max_files") ? this.ceph_stats.max_files : "N/A" + ); + extra_properties += property_entry_html( + "Max bytes", + this.ceph_stats.hasOwnProperty("max_bytes") ? this.ceph_stats.max_bytes : "N/A" + ); + } + super.show_properties(extra_properties); + } +} + +export class NavDirLink extends NavDir{ + /** + * + * @param {string|string[]} path + * @param {object} stat + * @param {NavWindow} nav_window_ref + * @param {string} link_target + */ + constructor(path, stat, nav_window_ref, link_target) { + super(path, stat, nav_window_ref); + var link_icon = this.dom_element.nav_item_icon.link_icon = document.createElement("i"); + link_icon.classList.add("fas", "fa-link", "nav-item-symlink-symbol-dir"); + this.dom_element.nav_item_icon.appendChild(link_icon); + this.double_click = false; + this.link_target = link_target; + this.dom_element.nav_item_title.style.fontStyle = "italic"; + if (nav_window_ref.item_display === "list") + this.dom_element.nav_item_title.innerHTML += " → " + this.link_target; + } + + /** + * + * @returns {Promise} + */ + rm() { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["rm", "-f", this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }) + proc.fail((e, data) => { + reject(data); + }); + }); + } + + show_properties() { + var extra_properties = property_entry_html("Link Target", this.link_target); + super.show_properties(extra_properties); + } +} diff --git a/navigator/components/NavDownloader.js b/navigator/components/NavDownloader.js new file mode 100644 index 0000000..fa64df7 --- /dev/null +++ b/navigator/components/NavDownloader.js @@ -0,0 +1,58 @@ +/* + 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 {NavFile} from "./NavFile.js"; + +export class NavDownloader { + /** + * + * @param {NavFile} file + */ + constructor(file) { + this.path = file.path_str(); + this.filename = file.filename(); + this.read_size = file.stat["size"]; + } + + async download() { + let query = window.btoa(JSON.stringify({ + payload: 'fsread1', + binary: 'raw', + path: this.path, + superuser: true, + max_read_size: this.read_size, + external: { + 'content-disposition': 'attachment; filename="' + this.filename + '"', + 'content-type': 'application/x-xz, application/octet-stream' + }, + })); + let prefix = (new URL(cockpit.transport.uri('channel/' + cockpit.transport.csrf_token))).pathname; + var a = document.createElement("a"); + a.href = prefix + "?" + query; + a.style.display = "none"; + a.download = this.filename; + document.body.appendChild(a); + var event = new MouseEvent('click', { + 'view': window, + 'bubbles': false, + 'cancelable': true + }); + a.dispatchEvent(event); + } +} diff --git a/navigator/components/NavDragDrop.js b/navigator/components/NavDragDrop.js new file mode 100644 index 0000000..3ffc75f --- /dev/null +++ b/navigator/components/NavDragDrop.js @@ -0,0 +1,91 @@ +/* + 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 NavDragDrop { + /** + * + * @param {HTMLDivElement} drop_area + * @param {NavWindow} nav_window_ref + */ + constructor(drop_area, nav_window_ref) { + drop_area.addEventListener("dragenter", this); + drop_area.addEventListener("dragover", this); + drop_area.addEventListener("dragleave", this); + drop_area.addEventListener("drop", this); + this.drop_area = drop_area; + this.nav_window_ref = nav_window_ref; + } + + handleEvent(e) { + e.preventDefault(); + switch(e.type){ + case "dragenter": + this.drop_area.classList.add("drag-enter"); + break; + case "dragover": + break; + case "dragleave": + this.drop_area.classList.remove("drag-enter"); + break; + case "drop": + if (e.dataTransfer.items) { + for (let item of e.dataTransfer.items) { + if (item.kind === 'file') { + var file = item.getAsFile(); + if (file.type === "" && file.size !== 0) { + this.nav_window_ref.modal_prompt.alert(file.name + ": Cannot upload folders."); + continue; + } + if (file.size === 0) { + var proc = cockpit.spawn( + ["/usr/share/cockpit/navigator/scripts/touch.py3", this.nav_window_ref.pwd().path_str() + "/" + file.name], + {superuser: "try", err: "out"} + ); + proc.done(() => { + this.nav_window_ref.refresh(); + }); + proc.fail((e, data) => { + this.nav_window_ref.modal_prompt.alert(data); + }); + } else { + var uploader = new FileUpload(file, this.nav_window_ref); + uploader.upload(); + } + } + } + } else { + for (let file of ev.dataTransfer.files) { + if (file.type === "") + continue; + var uploader = new FileUpload(file, this.nav_window_ref); + uploader.upload(); + } + } + this.drop_area.classList.remove("drag-enter"); + break; + default: + this.drop_area.classList.remove("drag-enter"); + break; + } + e.stopPropagation(); + } +} diff --git a/navigator/components/NavEntry.js b/navigator/components/NavEntry.js new file mode 100644 index 0000000..45eef0e --- /dev/null +++ b/navigator/components/NavEntry.js @@ -0,0 +1,267 @@ +/* + 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 {NavWindow} from "./NavWindow.js"; +import {format_bytes, property_entry_html, format_time} from "../functions.js"; + +export class NavEntry { + /** + * + * @param {string} path + * @param {object} stat + * @param {NavWindow} nav_window_ref + */ + constructor(path, stat, nav_window_ref) { + this.nav_window_ref = nav_window_ref; + if (typeof path == "string") + this.path = path.split("/").splice(1); + else + this.path = (path.length) ? path : [""]; + 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(); + this.dom_element.appendChild(icon); + this.dom_element.appendChild(title); + this.stat = stat; + if (stat && stat["inaccessible"]) { + this.dom_element.style.cursor = "not-allowed"; + } else { + this.dom_element.addEventListener("click", this); + this.dom_element.addEventListener("contextmenu", this); + } + this.is_hidden_file = this.filename().startsWith('.'); + if (this.is_hidden_file) + icon.style.opacity = 0.5; + 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"); + let group = document.createElement("div"); + let size = document.createElement("div"); + mode.title = mode.innerText = this.stat["mode-str"]; + owner.title = owner.innerText = this.stat["owner"]; + group.title = group.innerText = this.stat["group"]; + size.title = size.innerText = format_bytes(this.stat["size"]); + mode.classList.add("nav-item-title", "no-select", "monospace-sm"); + owner.classList.add("nav-item-title", "no-select"); + group.classList.add("nav-item-title", "no-select"); + size.classList.add("nav-item-title", "no-select"); + this.dom_element.appendChild(mode); + this.dom_element.appendChild(owner); + this.dom_element.appendChild(group); + this.dom_element.appendChild(size); + } + } + + /** + * + * @param {Event} e + */ + handleEvent(e) { + switch (e.type) { + case "click": + this.nav_window_ref.set_selected(this, e.shiftKey, e.ctrlKey); + this.context_menu_ref.hide(); + e.stopPropagation(); + break; + case "contextmenu": + this.context_menu_ref.show(e, this); + e.preventDefault(); + e.stopPropagation(); + break; + } + } + + destroy() { + while (this.dom_element.firstChild) { + this.dom_element.removeChild(this.dom_element.firstChild); + } + if (this.dom_element.parentElement) + this.dom_element.parentElement.removeChild(this.dom_element); + } + + /** + * + * @returns {string} + */ + filename() { + var name = this.path[this.path.length - 1]; + if (!name) + name = "/"; + return name; + } + + /** + * + * @returns {string} + */ + path_str() { + return "/" + this.path.join("/"); + } + + /** + * + * @returns {string} + */ + parent_dir() { + return this.path.slice(0, this.path.length - 1); + } + + show() { + this.dom_element.style.display = "flex"; + } + + hide() { + this.dom_element.style.display = "none"; + } + + /** + * + * @returns {object} + */ + get_properties() { + return this.stat; + } + + /** + * + * @returns {number} + */ + get_permissions() { + return this.stat["mode"] & 0o777; + } + + /** + * + * @param {number} new_perms + * @returns {Promise} + */ + chmod(new_perms) { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["chmod", (new_perms & 0o777).toString(8), this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + } + + /** + * + * @param {string} new_owner + * @param {string} new_group + * @returns {Promise} + */ + chown(new_owner, new_group) { + return new Promise((resolve, reject) => { + if (!new_owner && !new_group) { + resolve(); + return; + } + var cmd = ""; + var arg = ""; + if (new_group && !new_owner) { + cmd = "chgrp"; + arg = new_group; + } else { + cmd = "chown"; + arg = new_owner; + if (new_group) + arg += ":" + new_group; + } + var proc = cockpit.spawn( + [cmd, arg, this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + } + + /** + * + * @param {string} new_path + * @returns {Promise} + */ + mv(new_path) { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["mv", "-n", this.path_str(), [this.nav_window_ref.pwd().path_str(), new_path].join("/")], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + } + + /** + * + * @param {string} extra_properties + */ + 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(); + } + var html = ""; + html += property_entry_html("Mode", this.stat["mode-str"]); + html += property_entry_html("Owner", this.stat["owner"] + " (" + this.stat["uid"] + ")"); + html += property_entry_html("Group", this.stat["group"] + " (" + this.stat["gid"] + ")"); + html += property_entry_html("Size", format_bytes(this.stat["size"])); + html += property_entry_html("Accessed", format_time(this.stat["atime"])); + html += property_entry_html("Modified", format_time(this.stat["mtime"])); + html += property_entry_html("Created", format_time(this.stat["ctime"])); + html += extra_properties; + document.getElementById("nav-info-column-properties").innerHTML = html; + } + + populate_edit_fields() { + document.getElementById("nav-edit-filename").innerText = this.filename(); + var mode_bits = [ + "other-exec", "other-write", "other-read", + "group-exec", "group-write", "group-read", + "owner-exec", "owner-write", "owner-read" + ]; + for (let i = 0; i < mode_bits.length; i++) { + var bit_check = 1 << i; + var result = this.stat["mode"] & bit_check; + document.getElementById(mode_bits[i]).checked = (result != 0); + } + document.getElementById("nav-edit-owner").value = this.stat["owner"]; + document.getElementById("nav-edit-group").value = this.stat["group"]; + } +} diff --git a/navigator/components/NavFile.js b/navigator/components/NavFile.js new file mode 100644 index 0000000..199d6db --- /dev/null +++ b/navigator/components/NavFile.js @@ -0,0 +1,235 @@ +/* + 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 {NavEntry} from "./NavEntry.js"; +import {NavDownloader} from "./NavDownloader.js"; +import {NavWindow} from "./NavWindow.js"; +import {property_entry_html} from "../functions.js"; + +export class NavFile extends NavEntry { + /** + * + * @param {string|string[]} path + * @param {object} stat + * @param {NavWindow} nav_window_ref + */ + constructor(path, stat, nav_window_ref) { + super(path, stat, nav_window_ref); + this.nav_type = "file"; + this.dom_element.nav_item_icon.classList.add("fas", "fa-file"); + this.double_click = false; + } + + /** + * + * @param {Event} e + */ + handleEvent(e) { + switch(e.type){ + case "click": + if (this.double_click) + this.open(); + else { // single click + this.double_click = true; + if(this.timeout) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.double_click = false; + }, 500); + } + break; + case "keydown": + if (e.keyCode === 83 && e.ctrlKey === true) { + e.preventDefault(); + e.stopPropagation(); + this.write_to_file(); + } + break; + } + super.handleEvent(e); + } + + /** + * + * @returns {Promise} + */ + rm() { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["rm", "-f", this.path_str()], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + } + + async open() { + var proc_output = await cockpit.spawn(["file", "--mime-type", this.path_str()], {superuser: "try"}); + var fields = proc_output.split(/:(?=[^:]+$)/); // ensure it's the last : with lookahead + var type = fields[1].trim(); + + if ((/^text/.test(type) || /^inode\/x-empty$/.test(type) || this.stat["size"] === 0)) { + 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?")) { + var download = new NavDownloader(this); + download.download(); + } + } + } + + async show_edit_file_contents() { + window.removeEventListener("keydown", this.nav_window_ref); + this.nav_window_ref.disable_buttons_for_editing(); + var contents = ""; + try { + contents = await cockpit.file(this.path_str(), {superuser: "try"}).read(); + } catch (e) { + this.nav_window_ref.enable_buttons(); + this.nav_window_ref.modal_prompt.alert(e.message); + return; + } + var text_area = document.getElementById("nav-edit-contents-textarea"); + text_area.value = contents; + text_area.addEventListener("keydown", this); + 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-edit-contents-view").style.display = "flex"; + } + + async write_to_file() { + var new_contents = document.getElementById("nav-edit-contents-textarea").value; + try { + if (new_contents.length) + await cockpit.file(this.path_str(), {superuser: "try"}).replace(new_contents); + else + await cockpit.script("echo -n > $1", [this.path_str()], {superuser: "try"}); + } catch (e) { + this.nav_window_ref.modal_prompt.alert(e.message); + } + this.nav_window_ref.refresh(); + this.hide_edit_file_contents(); + } + + hide_edit_file_contents() { + 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"; + this.nav_window_ref.enable_buttons(); + } +} + +export class NavFileLink extends NavFile{ + /** + * + * @param {string} path + * @param {object} stat + * @param {NavWindow} nav_window_ref + * @param {string} link_target + */ + constructor(path, stat, nav_window_ref, link_target) { + super(path, stat, nav_window_ref); + var link_icon = this.dom_element.nav_item_icon.link_icon = document.createElement("i"); + link_icon.classList.add("fas", "fa-link", "nav-item-symlink-symbol-file"); + this.dom_element.nav_item_icon.appendChild(link_icon); + this.double_click = false; + this.link_target = link_target; + this.dom_element.nav_item_title.style.fontStyle = "italic"; + if (nav_window_ref.item_display === "list") + this.dom_element.nav_item_title.innerHTML += " → " + this.link_target; + } + + show_properties() { + var extra_properties = property_entry_html("Link Target", this.link_target); + super.show_properties(extra_properties); + } + + /** + * + * @returns {string} + */ + get_link_target_path() { + var target = ""; + if (this.link_target.charAt(0) === '/') + target = this.link_target; + else + target = this.parent_dir().join("/") + "/" + this.link_target; + if (target.charAt(0) !== '/') + target = '/' + target; + return target; + } + + async open() { + var target_path = this.get_link_target_path(); + var proc_output = await cockpit.spawn(["file", "--mime-type", target_path], {superuser: "try"}); + var fields = proc_output.split(/:(?=[^:]+$)/); // ensure it's the last : with lookahead + var type = fields[1].trim(); + + if ((/^text/.test(type) || /^inode\/x-empty$/.test(type) || this.stat["size"] === 0)) { + 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."); + } + } + + async show_edit_file_contents() { + window.removeEventListener("keydown", this.nav_window_ref); + this.nav_window_ref.disable_buttons_for_editing(); + document.getElementById("pwd").disabled = true; + var target_path = this.get_link_target_path(); + var contents = ""; + try { + contents = await cockpit.file(target_path, {superuser: "try"}).read(); + } catch(e) { + this.nav_window_ref.enable_buttons(); + this.nav_window_ref.modal_prompt.alert(e.message); + return; + } + var text_area = document.getElementById("nav-edit-contents-textarea"); + text_area.value = contents; + text_area.addEventListener("keydown", this); + 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-edit-contents-view").style.display = "flex"; + } + + async write_to_file() { + var target_path = this.get_link_target_path(); + var new_contents = document.getElementById("nav-edit-contents-textarea").value; + try { + await cockpit.file(target_path, {superuser: "try"}).replace(new_contents); + } catch (e) { + this.nav_window_ref.modal_prompt.alert(e.message); + } + this.nav_window_ref.refresh(); + this.hide_edit_file_contents(); + } +} diff --git a/navigator/components/NavWindow.js b/navigator/components/NavWindow.js new file mode 100644 index 0000000..b7e3955 --- /dev/null +++ b/navigator/components/NavWindow.js @@ -0,0 +1,841 @@ +/* + 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 {NavEntry} from "./NavEntry.js"; +import {NavDir} from "./NavDir.js"; +import {NavContextMenu} from "./NavContextMenu.js"; +import {NavDragDrop} from "./NavDragDrop.js"; +import {SortFunctions} from "./SortFunctions.js"; +import {ModalPrompt} from "./ModalPrompt.js"; +import {format_bytes, format_permissions} from "../functions.js"; + +export class NavWindow { + constructor() { + this.item_display = "grid"; + this.path_stack = (localStorage.getItem('navigator-path') ?? '/').split('/'); + this.path_stack = this.path_stack.map((_, index) => new NavDir([...this.path_stack.slice(0, index + 1)].filter(part => part != ''), this)); + + this.path_stack_index = this.path_stack.length - 1; + this.selected_entries = new Set([this.pwd()]); + this.entries = []; + this.window = document.getElementById("nav-contents-view"); + 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); + + this.clip_board = []; + + this.uploader = new NavDragDrop(this.window, this); + + this.sort_function = new SortFunctions(); + + this.modal_prompt = new ModalPrompt(); + } + + /** + * + * @param {Event} e + */ + handleEvent(e) { + switch (e.type) { + case "click": + this.clear_selected(); + this.show_selected_properties(); + break; + case "contextmenu": + this.context_menu.show(e, this.pwd()); + e.preventDefault(); + break; + case "keydown": + if (e.keyCode === 46 && e.target === document.body) { + this.delete_selected(); + } else if (e.keyCode === 65 && e.ctrlKey) { + this.select_all(); + e.preventDefault(); + } else if (e.keyCode === 67 && e.ctrlKey) { + this.copy(); + } else if (e.keyCode === 86 && e.ctrlKey) { + this.paste(); + } else if (e.keyCode === 88 && e.ctrlKey) { + this.cut(); + } + break; + default: + break; + } + } + + async refresh() { + localStorage.setItem('navigator-path', `/${this.path_stack[this.path_stack_index].path.join('/')}`); + + var num_dirs = 0; + var num_files = 0; + var bytes_sum = 0; + this.show_hidden = document.getElementById("nav-show-hidden").checked; + this.start_load(); + try { + var files = await this.pwd().get_children(this); + } catch(e) { + this.up(); + this.modal_prompt.alert(e); + return; + } + while (this.entries.length) { + var entry = this.entries.pop(); + entry.destroy(); + } + files.sort((first, second) => { + if (first.nav_type === second.nav_type) { + return this.item_display === "list" + ? this.sort_function.get_func()(first, second) + : this.sort_function.name_asc(first, second); // default to sort by name in grid view + } + if (first.nav_type === "dir") + return -1; + return 1; + }); + files.forEach((file) => { + if (file.nav_type === "dir") + num_dirs++; + else { + num_files++; + bytes_sum += file.stat["size"]; + } + if(!file.is_hidden_file || this.show_hidden) { + this.window.appendChild(file.dom_element); + this.entries.push(file); + } + file.context_menu_ref = this.context_menu; + }); + document.getElementById("pwd").value = this.pwd().path_str(); + this.set_selected(this.pwd(), false, false); + this.show_selected_properties(); + document.getElementById("nav-num-dirs").innerText = num_dirs.toString(); + document.getElementById("nav-num-files").innerText = num_files.toString(); + document.getElementById("nav-num-bytes").innerText = format_bytes(bytes_sum); + this.stop_load(); + this.set_nav_button_state(); + } + + set_nav_button_state() { + document.getElementById("nav-back-btn").disabled = (this.path_stack_index === 1); + document.getElementById("nav-forward-btn").disabled = (this.path_stack_index === this.path_stack.length - 1); + document.getElementById("nav-up-dir-btn").disabled = (this.pwd().path_str() === "/"); + } + + /** + * + * @returns {NavDir} + */ + pwd() { + return this.path_stack[this.path_stack_index]; + } + + /** + * + * @param {NavDir} new_dir + */ + cd(new_dir) { + this.path_stack.length = this.path_stack_index + 1; + this.path_stack.push(new_dir); + this.path_stack_index = this.path_stack.length - 1; + this.refresh(); + } + + back() { + this.path_stack_index = Math.max(this.path_stack_index - 1, 0); + this.refresh(); + } + + forward() { + this.path_stack_index = Math.min(this.path_stack_index + 1, this.path_stack.length - 1); + this.refresh(); + } + + up() { + if(this.pwd().path_str() !== '/') + this.cd(new NavDir(this.pwd().parent_dir())); + } + + /** + * + * @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); + } + + clear_selected() { + this.set_selected(this.pwd(), false, false); + } + + selected_entry() { + return [...this.selected_entries][this.selected_entries.size - 1]; + } + + none_selected() { + return this.selected_entries.size === 1 && this.selected_entry() === this.pwd(); + } + + show_selected_properties() { + this.selected_entry().show_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)) { + dangerous_selected.push(path); + } + } + if (dangerous_selected.length > 0) { + var dangerous_selected_str = ""; + if (dangerous_selected.length > 2) { + var last = dangerous_selected.pop(); + dangerous_selected_str = dangerous_selected.join(", "); + dangerous_selected_str += ", and " + last; + } else if (dangerous_selected.length === 2) { + dangerous_selected_str = dangerous_selected.join(" and "); + } else { + dangerous_selected_str = dangerous_selected[0]; + } + if (!await this.modal_prompt.confirm( + "Warning: editing " + + dangerous_selected_str + + " can be dangerous.", + "Are you sure?", + true + )) { + return; + } + } else if (this.selected_entries.size > 1) { + if (!await this.modal_prompt.confirm( + "Warning: editing permissions for " + + this.selected_entries.size + + " files.", + "Are you sure?", + true + )) { + return; + } + } + if (this.selected_entries.size === 1) { + this.selected_entry().populate_edit_fields(); + document.getElementById("selected-files-list-header").innerText = ""; + document.getElementById("selected-files-list").innerText = ""; + } else { + for (let field of ["owner", "group"]) { + document.getElementById("nav-edit-" + field).value = ""; + } + var filename = document.getElementById("nav-edit-filename"); + filename.value = "N/A"; + filename.disabled = true; + for (let checkbox of document.getElementsByClassName("mode-checkbox")) { + checkbox.checked = false; + } + var targets = []; + for (let target of this.selected_entries) { + targets.push(target.filename()); + } + var targets_str = targets.join(", "); + document.getElementById("selected-files-list-header").innerText = "Applying edits to:"; + document.getElementById("selected-files-list").innerText = targets_str; + } + this.update_permissions_preview(); + this.changed_mode = false; + document.getElementById("nav-mode-preview").innerText = "unchanged"; + document.getElementById("nav-edit-properties").style.display = "flex"; + document.getElementById("nav-show-properties").style.display = "none"; + } + + hide_edit_selected() { + document.getElementById("nav-show-properties").style.display = "flex"; + document.getElementById("nav-edit-properties").style.display = "none"; + } + + /** + * + * @returns {number} + */ + get_new_permissions() { + var category_list = ["other", "group", "owner"]; + var action_list = ["exec", "write", "read"]; + var result = 0; + var bit = 0; + for (let category of category_list) { + for (let action of action_list) { + if (document.getElementById(category + "-" + action).checked) + result |= 1 << bit; + bit++; + } + } + return result; + } + + update_permissions_preview() { + var new_perms = this.get_new_permissions(); + var text = format_permissions(new_perms); + text += " (" + (new_perms & 0o777).toString(8).padStart(3, '0') + ")"; + document.getElementById("nav-mode-preview").innerText = text; + this.changed_mode = true; + } + + async apply_edit_selected() { + var new_owner = document.getElementById("nav-edit-owner").value; + var new_group = document.getElementById("nav-edit-group").value; + var new_perms = this.get_new_permissions(); + + for (let entry of this.selected_entries) { + if ( + new_owner !== entry.stat["owner"] || + new_group !== entry.stat["group"] + ) { + try { + await entry.chown(new_owner, new_group); + } catch(e) { + this.modal_prompt.alert(e); + } + } + if (this.changed_mode && (new_perms & 0o777) !== (entry.stat["mode"] & 0o777)) { + try { + await entry.chmod(new_perms); + } catch(e) { + this.modal_prompt.alert(e); + } + } + } + this.refresh(); + this.hide_edit_selected(); + } + + async delete_selected() { + var prompt = ""; + if (this.selected_entries.size > 1) { + prompt = "Deleting " + this.selected_entries.size + " files."; + } else { + prompt = "Deleting `" + this.selected_entry().path_str() + "`."; + } + if (!await this.modal_prompt.confirm(prompt, "This cannot be undone. Are you sure?", true)) { + return; + } + for (let target of this.selected_entries) { + try { + await target.rm(); + } catch(e) { + this.modal_prompt.alert(e); + } + } + this.refresh(); + } + + async mkdir() { + let response = await this.modal_prompt.prompt("Creating Directory", + { + new_name: { + label: "Name: ", + type: "text" + } + } + ); + if (response === null) + return; + var new_dir_name = response.new_name; + if (new_dir_name === "") { + this.modal_prompt.alert("Directory name can't be empty."); + return; + } + if (new_dir_name.includes("/")) { + this.modal_prompt.alert("Directory name can't contain `/`."); + return; + } + var promise = new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["mkdir", this.pwd().path_str() + "/" + new_dir_name], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + try { + await promise; + } catch(e) { + this.modal_prompt.alert(e); + } + this.refresh(); + } + + async touch() { + let response = await this.modal_prompt.prompt("Creating File", + { + new_name: { + label: "Name: ", + type: "text" + } + } + ); + if (response === null) + return; + var new_file_name = response.new_name; + if (new_file_name === "") { + this.modal_prompt.alert("File name can't be empty."); + return; + } + if (new_file_name.includes("/")) { + this.modal_prompt.alert("File name can't contain `/`."); + return; + } + var promise = new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["/usr/share/cockpit/navigator/scripts/touch.py3", this.pwd().path_str() + "/" + new_file_name], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + try { + await promise; + } catch(e) { + this.modal_prompt.alert(e); + } + this.refresh(); + } + + async ln(default_target = "") { + let response = await this.modal_prompt.prompt("Creating Symbolic Link", + { + target: { + label: "Target: ", + type: "text", + default: default_target + }, + name: { + label: "Name: ", + type: "text" + } + } + ); + if (response === null) + return; + var link_target = response.target; + if (link_target === "") { + this.modal_prompt.alert("Link target can't be empty."); + return; + } + var link_name = response.name; + if (link_name === "") { + this.modal_prompt.alert("Link name can't be empty."); + return; + } + if (link_name.includes("/")) { + this.modal_prompt.alert("Link name can't contain `/`."); + return; + } + var link_path = this.pwd().path_str() + "/" + link_name; + var promise = new Promise((resolve, reject) => { + var proc = cockpit.spawn( + ["ln", "-sn", link_target, link_path], + {superuser: "try", err: "out"} + ); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject(data); + }); + }); + try { + await promise; + } catch(e) { + this.modal_prompt.alert(e); + } + this.refresh(); + } + + cut() { + this.clip_board = [...this.selected_entries]; + this.copy_or_move = "move"; + this.paste_cwd = this.pwd().path_str(); + this.context_menu.menu_options["paste"].style.display = "flex"; + } + + copy() { + this.clip_board = [...this.selected_entries]; + this.copy_or_move = "copy"; + this.paste_cwd = this.pwd().path_str(); + this.context_menu.menu_options["paste"].style.display = "flex"; + } + + paste() { + this.paste_clipboard(); + this.context_menu.hide_paste(); + } + + async paste_clipboard() { + this.start_load(); + var cmd = ["/usr/share/cockpit/navigator/scripts/paste.py3"]; + var dest = this.pwd().path_str(); + if (this.copy_or_move === "move") { + cmd.push("-m"); + } + cmd.push(this.paste_cwd); + for (let item of this.clip_board) { + cmd.push(item.path_str()); + } + cmd.push(dest); + this.clip_board.length = 0; // clear clipboard + var promise = new Promise((resolve, reject) => { + var proc = cockpit.spawn( + cmd, + {superuser: "try", err: "ignore"} + ); + proc.stream(async (data) => { + var payload = JSON.parse(data); + if (payload["wants-response"]) { + if (payload.hasOwnProperty("conflicts")) { + let requests = {}; + for (let conflict of payload["conflicts"]) { + requests[conflict[0]] = { + label: conflict[1], + type: "checkbox", + default: false + } + } + this.stop_load(); + let responses = await this.modal_prompt.prompt("Overwrite?", requests); + this.start_load(); + if (responses === null) { + proc.input(JSON.stringify("abort") + "\n"); + return; + } + let keepers = []; + for (let response of Object.keys(responses)) { + if (responses[response]) + keepers.push(response) + } + proc.input(JSON.stringify(keepers) + "\n", true); + } else { + var user_response = await this.modal_prompt.confirm(payload["message"]); + proc.input(JSON.stringify(user_response) + "\n", true); + } + } else { + await this.modal_prompt.alert(payload["message"]); + } + }); + proc.done((data) => { + resolve(); + }); + proc.fail((e, data) => { + reject("Paste failed."); + }); + }); + try { + await promise; + } catch(e) { + this.modal_prompt.alert(e); + } + this.stop_load(); + this.refresh(); + } + + /** + * + * @param {Event} e + */ + nav_bar_event_handler(e) { + switch(e.key){ + case 'Enter': + this.nav_bar_cd(); + break; + default: + break; + } + } + + nav_bar_cd() { + var new_path = document.getElementById("pwd").value; + while (new_path.charAt(new_path.length - 1) === '/' && new_path.length > 1) + new_path = new_path.substr(0, new_path.length - 1); + this.cd(new NavDir(new_path)); + } + + async nav_bar_update_choices() { + var list = document.getElementById("possible-paths"); + var partial_path_str = document.getElementById("pwd").value; + var last_delim = partial_path_str.lastIndexOf('/'); + if(last_delim === -1) + return; + var parent_path_str = partial_path_str.slice(0, last_delim); + if(this.nav_bar_last_parent_path_str === parent_path_str) + return; + this.nav_bar_last_parent_path_str = parent_path_str; + var parent_dir = new NavDir(parent_path_str); + var objs; + try { + objs = await parent_dir.get_children(this); + } catch(e) { + return; + } + objs = objs.filter((child) => {return child.nav_type === "dir"}); + while(list.firstChild) + list.removeChild(list.firstChild); + objs.forEach((obj) => { + var option = document.createElement("option"); + option.value = obj.path_str(); + list.appendChild(option); + }); + } + + start_load() { + document.getElementById("nav-loader-container").style.display = "block"; + var buttons = document.getElementsByTagName("button"); + for (let button of buttons) { + button.disabled = true; + } + } + + stop_load() { + document.getElementById("nav-loader-container").style.display = "none"; + var buttons = document.getElementsByTagName("button"); + for (let button of buttons) { + button.disabled = false; + } + } + + /** + * + * @param {Event} e + */ + toggle_show_hidden(e) { + localStorage.setItem('show-hidden-files', e.target.checked); + + var icon = document.getElementById("nav-show-hidden-icon"); + if (e.target.checked) { + icon.classList.remove("fa-low-vision"); + icon.classList.add("fa-eye"); + } else { + icon.classList.remove("fa-eye"); + icon.classList.add("fa-low-vision"); + } + this.refresh(); + } + + /** + * + * @returns {Promise} + */ + get_system_users() { + return new Promise(async (resolve, reject) => { + var proc = cockpit.spawn(["getent", "passwd"], {err: "ignore", superuser: "try"}); + proc.fail((e, data) => { + reject(data); + }); + var list = document.getElementById("possible-owners"); + while(list.firstChild) { + list.removeChild(list.firstChild); + } + var passwd; + try { + passwd = await proc; + } catch(e) { + reject(e); + return; + } + var passwd_entries = passwd.split("\n"); + passwd_entries.sort((first, second) => { + return first.split(":")[0].localeCompare(second.split(":")[0]); + }); + for (let entry of passwd_entries) { + var cols = entry.split(":"); + var username = cols[0]; + var option = document.createElement("option"); + option.value = username; + list.appendChild(option); + } + resolve(); + }); + } + + /** + * + * @returns {Promise} + */ + get_system_groups() { + return new Promise(async (resolve, reject) => { + var proc = cockpit.spawn(["getent", "group"], {err: "ignore", superuser: "try"}); + proc.fail((e, data) => { + reject(data); + }); + var list = document.getElementById("possible-groups"); + while(list.firstChild) { + list.removeChild(list.firstChild); + } + var group + try { + group = await proc; + } catch(e) { + reject(e); + return; + } + var group_entries = group.split("\n"); + group_entries.sort((first, second) => { + return first.split(":")[0].localeCompare(second.split(":")[0]); + }); + for (let entry of group_entries) { + var cols = entry.split(":"); + var groupname = cols[0]; + var option = document.createElement("option"); + option.value = groupname; + list.appendChild(option); + } + resolve(); + }); + } + + disable_buttons_for_editing() { + for (let button of document.getElementsByTagName("button")) { + if (!button.classList.contains("editor-btn")) + button.disabled = true; + } + document.getElementById("pwd").disabled = true; + } + + enable_buttons() { + for (let button of document.getElementsByTagName("button")) { + button.disabled = false; + } + document.getElementById("pwd").disabled = false; + 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") { + this.item_display = "list"; + await this.refresh(); + this.window.classList.remove("contents-view-grid"); + this.window.classList.add("contents-view-list"); + button.classList.remove("fa-list"); + button.classList.add("fa-th"); + } else { + this.item_display = "grid"; + await this.refresh(); + this.window.classList.remove("contents-view-list"); + this.window.classList.add("contents-view-grid"); + button.classList.remove("fa-th"); + button.classList.add("fa-list"); + } + + localStorage.setItem("item-display", this.item_display); + } + + search_filter(event) { + var search_name = event.target.value; + this.entries.forEach((entry) => { + if (entry.filename().toLowerCase().startsWith(search_name.toLowerCase())) + entry.show(); + else + entry.hide(); + }); + } +} diff --git a/navigator/components/SortFunctions.js b/navigator/components/SortFunctions.js new file mode 100644 index 0000000..bb7845b --- /dev/null +++ b/navigator/components/SortFunctions.js @@ -0,0 +1,93 @@ +/* + 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 . + */ + +export class SortFunctions { + constructor() { + this.orders = { + name: "asc", + owner: "asc", + group: "asc", + size: "asc", + } + this.icons = {}; + for (let option of ["name", "owner", "group", "size"]) { + this.icons[option] = document.getElementById(`sort-${option}-icon`); + } + this.current_choice = "name"; + } + + get_func() { + return this[`${this.current_choice}_${this.orders[this.current_choice]}`]; + } + + set_func(option) { + if (this.current_choice === option) { + if (this.orders[this.current_choice] === "asc") { + this.orders[this.current_choice] = "desc"; + this.icons[this.current_choice].classList.remove("fa-chevron-up"); + this.icons[this.current_choice].classList.add("fa-chevron-down"); + } else { + this.orders[this.current_choice] = "asc"; + this.icons[this.current_choice].classList.remove("fa-chevron-down"); + this.icons[this.current_choice].classList.add("fa-chevron-up"); + } + } else { + this.icons[this.current_choice].classList.remove("fa-chevron-up", "fa-chevron-down"); + this.current_choice = option; + if (this.orders[this.current_choice] === "asc") { + this.icons[this.current_choice].classList.add("fa-chevron-up"); + } else { + this.icons[this.current_choice].classList.add("fa-chevron-down"); + } + } + + } + + name_asc(first, second) { + return first.filename().localeCompare(second.filename()); + } + + name_desc(first, second) { + return second.filename().localeCompare(first.filename()); + } + + owner_asc(first, second) { + return first.stat["owner"].localeCompare(second.stat["owner"]); + } + + owner_desc(first, second) { + return second.stat["owner"].localeCompare(first.stat["owner"]); + } + + group_asc(first, second) { + return first.stat["group"].localeCompare(second.stat["group"]); + } + + group_desc(first, second) { + return second.stat["group"].localeCompare(first.stat["group"]); + } + + size_asc(first, second) { + return first.stat["size"] - second.stat["size"]; + } + + size_desc(first, second) { + return second.stat["size"] - first.stat["size"]; + } +} diff --git a/navigator/functions.js b/navigator/functions.js new file mode 100644 index 0000000..cbada10 --- /dev/null +++ b/navigator/functions.js @@ -0,0 +1,98 @@ +/* + 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 . + */ + +/** + * + * @param {string} key + * @param {string} value + * @returns {string} + */ + export function property_entry_html(key, value) { + var html = ''; + return html; +} + +/** + * + * @param {number} bytes + * @returns {string} + */ +export function format_bytes(bytes) { + if (bytes === 0) + return "0 B"; + var units = [" B", " KiB", " MiB", " GiB", " TiB", " PiB"]; + var index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); + var pow = Math.pow(1024, index); + var formatted = bytes / pow; + return formatted.toFixed(2).toString() + units[index]; +} + +/** + * + * @param {number} timestamp + * @returns {string} + */ +export function format_time(timestamp) { + var date = new Date(timestamp * 1000); + return date.toLocaleString(); +} + +/** + * + * @param {number} seconds + * @returns {string} + */ +export function format_time_remaining(seconds_) { + var hours = Math.floor(seconds_ / 3600); + var seconds = seconds_ % 3600; + var minutes = Math.floor(seconds / 60); + seconds = Math.floor(seconds % 60); + var out = ""; + if (hours) { + out = String(hours).padStart(2, '0') + ":"; + } + if (minutes) { + out += String(minutes).padStart(2, '0') + ":"; + } + out += String(seconds).padStart(2, '0'); + return out; +} + +/** + * + * @param {number} mode + * @returns {string} + */ +export function format_permissions(mode) { + var bit_list = ["x", "w", "r"]; + var result = ""; + for (let bit = 8; bit >= 0; bit--) { + var test_bit = 1 << bit; + var test_result = mode & test_bit; + if (test_result != 0) { + result += bit_list[bit % bit_list.length]; + } else { + result += "-"; + } + } + return result; +} \ No newline at end of file diff --git a/navigator/navigator.css b/navigator/navigator.css index f5e9b73..eee3968 100644 --- a/navigator/navigator.css +++ b/navigator/navigator.css @@ -63,6 +63,19 @@ body { height: 100%; } +.modal-content { + background-color: var(--container) !important; + color: var(--font) !important; +} + +.modal-header { + background-color: var(--container) !important; +} + +.modal-body { + background-color: var(--container) !important; +} + [data-theme="dark"] .pf-c-button:disabled { background-color: var(--container) !important; border: 1px solid var(--border) !important; @@ -111,7 +124,7 @@ body { 100% { transform: rotate(360deg); } } -body::-webkit-scrollbar { +::-webkit-scrollbar { width: 11px; } @@ -120,11 +133,12 @@ body { scrollbar-color: var(--scrollbar-color); } -body::-webkit-scrollbar-track { +::-webkit-scrollbar-track { background: var(--scrollbar-bg); + border-radius: var(--nav-border-radius); } -body::-webkit-scrollbar-thumb { +::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb) ; border-radius: 6px; border: 3px solid var(--scrollbar-bg); @@ -454,6 +468,10 @@ input[type="text"] { margin-left: 12px; } +.monospace { + font-family: 'Courier New', Courier, monospace; +} + .monospace-sm { font-family: 'Courier New', Courier, monospace; font-size: 80%; diff --git a/navigator/navigator.html b/navigator/navigator.html index 5df2ad9..4d7a51d 100644 --- a/navigator/navigator.html +++ b/navigator/navigator.html @@ -20,6 +20,7 @@ Navigator + @@ -28,7 +29,7 @@ - +