diff --git a/README.md b/README.md index 2ba392d..8bbe15a 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,17 @@ With no command line use needed, you can: # Installation ## From Github Release ### Ubuntu -1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.2.3/cockpit-navigator_0.2.3-1focal_all.deb` -1. `# apt install ./cockpit-navigator_0.2.3-1focal_all.deb` +1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator_0.3.0-1focal_all.deb` +1. `# apt install ./cockpit-navigator_0.3.0-1focal_all.deb` ### EL7 -1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.2.3/cockpit-navigator-0.2.3-1.el7.noarch.rpm` +1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator-0.3.0-1.el7.noarch.rpm` ### EL8 -1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.2.3/cockpit-navigator-0.2.3-1.el8.noarch.rpm` +1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator-0.3.0-1.el8.noarch.rpm` ## From Source 1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`. 1. `$ git clone https://github.com/45Drives/cockpit-navigator.git` 1. `$ cd cockpit-navigator` -1. `$ git checkout ` (v0.2.3 is latest) +1. `$ git checkout ` (v0.3.0 is latest) 1. `# make install` ## From 45Drives Repositories ### Ubuntu diff --git a/debian/changelog b/debian/changelog index 30442a2..a91c7c9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +cockpit-navigator (0.3.0-1focal) focal; urgency=medium + + * Add drag and drop uploading of files. + * Add event listeners for ctrl+a to select all, ctrl+x to cut, + ctrl+c to copy, ctrl+v to paste, and delete to remove a file. + + -- Josh Boudreau Thu, 03 Jun 2021 16:33:00 -0300 + cockpit-navigator (0.2.3-1focal) focal; urgency=medium * Fix closing contextmenu in el7. diff --git a/el/cockpit-navigator.spec b/el/cockpit-navigator.spec index 6a67c1f..45496f4 100644 --- a/el/cockpit-navigator.spec +++ b/el/cockpit-navigator.spec @@ -1,5 +1,5 @@ Name: cockpit-navigator -Version: 0.2.3 +Version: 0.3.0 Release: 1%{?dist} Summary: A File System Browser for Cockpit. License: GPL-3.0+ @@ -32,6 +32,10 @@ rm -rf %{buildroot} /usr/share/cockpit/navigator/* %changelog +* Thu Jun 03 2021 Josh Boudreau 0.3.0-1 +- Add drag and drop uploading of files. +- Add event listeners for ctrl+a to select all, ctrl+x to cut, + ctrl+c to copy, ctrl+v to paste, and delete to remove a file. * Wed Jun 02 2021 Josh Boudreau 0.2.3-1 - Fix closing contextmenu in el7. - Hide rename in right click menu with multiple selected entries. diff --git a/navigator/navigator.css b/navigator/navigator.css index 9c4465c..5a8821c 100644 --- a/navigator/navigator.css +++ b/navigator/navigator.css @@ -207,6 +207,7 @@ input[type="text"] { align-items: flex-start; align-content: flex-start; overflow: auto; + position: relative; } .nav-item { @@ -486,3 +487,45 @@ input:checked + .slider:before { .nav-context-menu-item:hover { background-color: var(--border); } + +.drag-enter { + border: 1px dashed var(--border); +} + +.nav-notifications { + position: absolute; + bottom: 0; + right: 0; + padding: 5px; + display: flex; + flex-flow: column-reverse nowrap; + align-items: stretch; + max-height: 50%; + overflow-y: auto; +} + +.nav-notification { + margin: 5px; + position: relative; + display: flex; + flex-flow: column nowrap; + align-items: stretch; + z-index: 10; + flex-grow: 0; + padding: 5px; + background-color: var(--container); + border-radius: var(--nav-border-radius); + color: var(--font); +} + +.nav-notification-header { + position: relative; + z-index: 10; + font-weight: bold; +} + +.nav-notification-header > progress { + position: relative; + z-index: 10; +} + diff --git a/navigator/navigator.html b/navigator/navigator.html index 621db41..af53ca4 100644 --- a/navigator/navigator.html +++ b/navigator/navigator.html @@ -64,7 +64,10 @@
- +
- + 45Drives
diff --git a/navigator/navigator.js b/navigator/navigator.js index e697c03..9f24f92 100644 --- a/navigator/navigator.js +++ b/navigator/navigator.js @@ -164,6 +164,8 @@ class NavEntry { 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(); } @@ -757,22 +759,15 @@ class NavContextMenu { } cut() { - this.nav_window_ref.clip_board = [...this.nav_window_ref.selected_entries]; - this.nav_window_ref.copy_or_move = "move"; - this.nav_window_ref.paste_cwd = this.nav_window_ref.pwd().path_str(); - this.menu_options["paste"].hidden = false; + this.nav_window_ref.cut(); } copy() { - this.nav_window_ref.clip_board = [...this.nav_window_ref.selected_entries]; - this.nav_window_ref.copy_or_move = "copy"; - this.nav_window_ref.paste_cwd = this.nav_window_ref.pwd().path_str(); - this.menu_options["paste"].hidden = false; + this.nav_window_ref.copy(); } paste() { - this.nav_window_ref.paste_clipboard(); - this.hide_paste(); + this.nav_window_ref.paste(); } rename() { @@ -809,18 +804,18 @@ class NavContextMenu { this.nav_window_ref.set_selected(target, false, false); } if (target === this.nav_window_ref.pwd()) { - this.menu_options["copy"].hidden = true; - this.menu_options["cut"].hidden = true; - this.menu_options["delete"].hidden = true; + this.menu_options["copy"].style.display = "none"; + this.menu_options["cut"].style.display = "none"; + this.menu_options["delete"].style.display = "none"; } else { - this.menu_options["copy"].hidden = false; - this.menu_options["cut"].hidden = false; - this.menu_options["delete"].hidden = false; + this.menu_options["copy"].style.display = "block"; + this.menu_options["cut"].style.display = "block"; + this.menu_options["delete"].style.display = "block"; } if (this.nav_window_ref.selected_entries.size > 1) { - this.menu_options["rename"].hidden = true; + this.menu_options["rename"].style.display = "none"; } else { - this.menu_options["rename"].hidden = false; + this.menu_options["rename"].style.display = "block"; } this.target = target; this.dom_element.style.display = "inline"; @@ -833,7 +828,189 @@ class NavContextMenu { } hide_paste() { - this.menu_options["paste"].hidden = true; + this.menu_options["paste"].style.display = "none"; + } +} + +class FileUpload { + /** + * + * @param {File|Blob} file + * @param {Number} chunk_size + * @param {NavWindow} nav_window_ref + */ + constructor(file, chunk_size, nav_window_ref) { + this.chunk_size = chunk_size; + 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; + } + + check_if_exists() { + return new Promise((resolve, reject) => { + var proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/fail-if-exists.py", 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 progress = document.createElement("progress"); + progress.max = this.num_chunks; + notification.appendChild(progress); + this.progress = progress; + this.html_elements = [progress, header, notification]; + document.getElementById("nav-notifications").appendChild(notification); + } + + remove_html_element() { + for (let elem of this.html_elements) { + elem.parentElement.removeChild(elem); + } + } + + /** + * + * @param {File|Blob} file + * @returns {Array} + */ + slice_file(file) { + var offset = 0; + var chunks = []; + this.num_chunks = Math.ceil(file.size / this.chunk_size); + for (let i = 0; i < this.num_chunks; i++) { + var 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()) { + window.alert(this.filename + ": File exists."); + return; + } + this.make_html_element(); + this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py", this.path], {err: "out", superuser: "try"}); + this.proc.fail((e, data) => { + this.reader.onload = () => {} + this.done(); + window.alert(data); + }) + this.proc.done((data) => { + + }) + 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 + * @param {Number} offset + */ + write_to_file(evt, offset) { + 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); + } + + done() { + this.proc.input(); // close stdin + this.nav_window_ref.refresh(); + this.remove_html_element(); + } +} + +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 === "") { + window.alert(file.name + ": Cannot upload folders."); + continue; + } + var uploader = new FileUpload(file, 4096, this.nav_window_ref); + uploader.upload(); + } + } + } else { + for (let file of ev.dataTransfer.files) { + if (file.type === "") + continue; + var uploader = new FileUpload(file, 4096, 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(); } } @@ -848,11 +1025,14 @@ class NavWindow { 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); } /** @@ -869,6 +1049,22 @@ class NavWindow { this.context_menu.show(e, this.pwd()); e.preventDefault(); break; + case "keydown": + if (e.keyCode === 46) { + 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; } } @@ -878,7 +1074,7 @@ class NavWindow { var num_dirs = 0; var num_files = 0; var bytes_sum = 0; - var show_hidden = document.getElementById("nav-show-hidden").checked; + this.show_hidden = document.getElementById("nav-show-hidden").checked; this.start_load(); var files = await this.pwd().get_children(this); while (this.entries.length) { @@ -900,7 +1096,7 @@ class NavWindow { num_files++; bytes_sum += file.stat["size"]; } - if(!file.is_hidden_file || show_hidden) + if(!file.is_hidden_file || this.show_hidden) file.show(); this.entries.push(file); file.context_menu_ref = this.context_menu; @@ -1224,6 +1420,25 @@ class NavWindow { 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 = "block"; + } + + 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 = "block"; + } + + paste() { + this.paste_clipboard(); + this.context_menu.hide_paste(); + } + async paste_clipboard() { this.start_load(); this.context_menu.hide_paste(); @@ -1253,9 +1468,10 @@ class NavWindow { proc.fail((e, data) => { window.alert("Paste failed."); }); - await proc; - this.stop_load(); - this.refresh(); + proc.always(() => { + this.stop_load(); + this.refresh(); + }); } /** @@ -1305,7 +1521,7 @@ class NavWindow { } start_load() { - document.getElementById("nav-loader-container").hidden = false; + document.getElementById("nav-loader-container").style.display = "block"; var buttons = document.getElementsByTagName("button"); for (let button of buttons) { button.disabled = true; @@ -1313,7 +1529,7 @@ class NavWindow { } stop_load() { - document.getElementById("nav-loader-container").hidden = true; + document.getElementById("nav-loader-container").style.display = "none"; var buttons = document.getElementsByTagName("button"); for (let button of buttons) { button.disabled = false; @@ -1386,6 +1602,14 @@ class NavWindow { } document.getElementById("pwd").disabled = false; } + + select_all() { + for (let entry of this.entries) { + if (!entry.is_hidden_file || this.show_hidden) { + this.set_selected(entry, false, true); + } + } + } } let nav_window = new NavWindow(); diff --git a/navigator/scripts/fail-if-exists.py b/navigator/scripts/fail-if-exists.py new file mode 100755 index 0000000..39cdcf6 --- /dev/null +++ b/navigator/scripts/fail-if-exists.py @@ -0,0 +1,26 @@ +#!/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 . +""" + +import os +import sys + +if os.path.exists(sys.argv[1]): + sys.exit(1) + +sys.exit(0) diff --git a/navigator/scripts/write-chunks.py b/navigator/scripts/write-chunks.py new file mode 100755 index 0000000..acec7e0 --- /dev/null +++ b/navigator/scripts/write-chunks.py @@ -0,0 +1,70 @@ +#!/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 . +""" + +""" +Synopsis: `write-chunks.py ` +JSON objects are of form: +obj = { + seek: + chunk: +} +""" + +import base64 +import os +import sys +import json + +def write_chunk(chunk, file): + seek = chunk["seek"] + data = base64.b64decode(chunk["chunk"]) + file.seek(seek) + file.write(data) + +def main(): + if len(sys.argv) != 2: + print("Invalid number of arguments.") + sys.exit(1) + path = sys.argv[1] + try: + file = open(path, "xb") + except Exception as e: + print(e) + sys.exit(1) + while True: + try: + json_in = input() + except EOFError: + break + json_list = json_in.split("\n") + for json_obj in json_list: + try: + obj_in = json.loads(json_obj) + except Exception as e: + print(e) + log = open("/var/log/navigator.log", "w") + log.write(json_in) + log.close() + sys.exit(1) + write_chunk(obj_in, file) + file.close() + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file