Merge pull request #12 from 45Drives/dev-josh

Changes for v0.3.0
This commit is contained in:
Josh Boudreau 2021-06-03 16:35:56 -03:00 committed by GitHub
commit f4d6f3e29f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 412 additions and 34 deletions

View File

@ -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 <version>` (v0.2.3 is latest)
1. `$ git checkout <version>` (v0.3.0 is latest)
1. `# make install`
## From 45Drives Repositories
### Ubuntu

8
debian/changelog vendored
View File

@ -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 <jboudreau@45drives.com> Thu, 03 Jun 2021 16:33:00 -0300
cockpit-navigator (0.2.3-1focal) focal; urgency=medium
* Fix closing contextmenu in el7.

View File

@ -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 <jboudreau@45drives.com> 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 <jboudreau@45drives.com> 0.2.3-1
- Fix closing contextmenu in el7.
- Hide rename in right click menu with multiple selected entries.

View File

@ -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;
}

View File

@ -64,7 +64,10 @@
</div>
<div class="vertical-spacer"></div>
<div class="flex-row inner-container">
<div class="contents-view" id="nav-contents-view"></div>
<div class="contents-view" id="nav-contents-view">
<div class="nav-notifications" id="nav-notifications">
</div>
</div>
<div class="edit-file-contents nav-hidden" id="nav-edit-contents-view">
<div class="editor-header" id="nav-edit-contents-header"></div>
<div class="vertical-spacer"></div>
@ -150,7 +153,7 @@
<span id="nav-num-dirs">-</span> Directories, <span id="nav-num-files">-</span> Files (<span id="nav-num-bytes">-</span>)
</div>
<div class="flex-grow"></div>
<a href="https://45drives.com" target="_blank" title="Visit 45Drives.com">
<a href="https://www.45drives.com/?utm_source=Houston&utm_medium=UI&utm_campaign=OS-Link" target="_blank" title="Visit 45Drives.com">
<img src="branding/logo-light.svg" id="logo-45d"><span class="logo-45">45</span><span class="logo-drives">Drives</span>
</a>
<div class="flex-grow"></div>

View File

@ -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();

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
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 <https://www.gnu.org/licenses/>.
"""
import os
import sys
if os.path.exists(sys.argv[1]):
sys.exit(1)
sys.exit(0)

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
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 <https://www.gnu.org/licenses/>.
"""
"""
Synopsis: `write-chunks.py <newline delimited JSON objects>`
JSON objects are of form:
obj = {
seek: <byte offset>
chunk: <base64 encoded data 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()