Release v0.5.4

This commit is contained in:
joshuaboud 2021-07-20 16:08:17 -03:00
commit 79ba268f7b
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
22 changed files with 534 additions and 194 deletions

View File

@ -1,4 +1,6 @@
## Cockpit Navigator 0.5.3-1
## Cockpit Navigator 0.5.4-1
* Implement inline filename editing.
* Add information popup button.
* Add fuzzy search.
* Optimize folder uploads.
* Fix bugs with selecting entries and renaming files.
* Stop user from deleting or renaming system-critical paths.

View File

@ -23,28 +23,28 @@ With no command line use needed, you can:
# Installation
## From Github Release
### Ubuntu
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator_0.5.3-1focal_all.deb`
1. `# apt install ./cockpit-navigator_0.5.3-1focal_all.deb`
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator_0.5.4-1focal_all.deb`
1. `# apt install ./cockpit-navigator_0.5.4-1focal_all.deb`
### EL7
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator-0.5.3-1.el7.noarch.rpm`
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator-0.5.4-1.el7.noarch.rpm`
### EL8
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.3/cockpit-navigator-0.5.3-1.el8.noarch.rpm`
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.4/cockpit-navigator-0.5.4-1.el8.noarch.rpm`
## From Source
1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`, `zip`.
1. `$ git clone https://github.com/45Drives/cockpit-navigator.git`
1. `$ cd cockpit-navigator`
1. `$ git checkout <version>` (v0.5.2 is latest)
1. `$ git checkout <version>` (v0.5.4 is latest)
1. `# make install`
## From 45Drives Repositories
### Ubuntu
1. Import GPG Key
```sh
wget -qO - http://repo.45drives.com/key.asc | sudo apt-key add -
wget -qO - https://repo.45drives.com/key/gpg.asc | gpg --dearmor -o /usr/share/keyrings/45drives-archive-keyring.gpg
```
2. Add 45drives.list
2. Add 45drives.sources
```sh
cd /etc/apt/sources.list.d
sudo wget http://repo.45drives.com/debian/45drives.list
curl -sSL https://repo.45drives.com/lists/45drives.sources -o /etc/apt/sources.list.d/45drives.sources
sudo apt update
```
3. Install Package
@ -52,10 +52,9 @@ sudo apt update
sudo apt install cockpit-navigator
```
### EL7/EL8
1. Add Repository
1. Add 45drives.repo
```sh
cd /etc/yum.repos.d
sudo wget http://repo.45drives.com/rhel/45drives.repo
curl -sSL https://repo.45drives.com/lists/45drives.repo -o /etc/yum.repos.d/45drives.repo
sudo yum clean all
```
2. Install Package

View File

@ -3,7 +3,7 @@
"name": "cockpit-navigator",
"title": "Cockpit Navigator",
"prerelease": false,
"version": "0.5.3",
"version": "0.5.4",
"buildVersion": "1",
"author": "Josh Boudreau <jboudreau@45drives.com>",
"url": "https://github.com/45Drives/cockpit-navigator",
@ -54,7 +54,7 @@
],
"changelog": {
"urgency": "medium",
"version": "0.5.3",
"version": "0.5.4",
"buildVersion": "1",
"ignore": [],
"date": null,

View File

@ -41,34 +41,30 @@ export class FileUpload {
this.reader = new FileReader();
this.chunks = this.slice_file(file);
this.chunk_index = 0;
this.timestamp = Date.now();
this.modal_prompt = new ModalPrompt();
this.using_webkit = true;
}
check_if_exists() {
return new Promise((resolve, reject) => {
var proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/fail-if-exists.py3", this.path], {superuser: "try"});
proc.done((data) => {resolve(false)});
proc.fail((e, data) => {resolve(true)});
});
this.make_html_element();
}
make_html_element() {
var notification = document.createElement("div");
var notification = this.dom_element = document.createElement("div");
notification.classList.add("nav-notification");
var header = document.createElement("div");
header.classList.add("nav-notification-header");
notification.appendChild(header);
header.innerText = "Uploading " + this.filename;
header.style.position = "relative";
header.style.paddingRight = "1em";
header.style.display = "grid";
header.style.gridTemplateColumns = "1fr 20px";
header.style.gap = "5px";
var title = document.createElement("p");
title.innerText = "Uploading " + this.filename;
title.title = this.filename;
var cancel = document.createElement("i");
cancel.classList.add("fa", "fa-times");
cancel.style.position = "absolute"
cancel.style.right = "0";
cancel.style.justifySelf = "center";
cancel.style.alignSelf = "center";
cancel.style.cursor = "pointer";
cancel.onclick = () => {
if (this.proc) {
@ -76,7 +72,8 @@ export class FileUpload {
this.done();
}
}
header.appendChild(cancel);
header.append(title, cancel);
var info = document.createElement("div");
info.classList.add("flex-row", "space-between");
@ -129,7 +126,8 @@ export class FileUpload {
}
async upload() {
this.make_html_element();
this.timestamp = Date.now();
this.dom_element.style.display = "flex";
this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py3", this.path], {err: "out", superuser: "try"});
this.proc.fail((e, data) => {
this.reader.onload = () => {}
@ -137,8 +135,10 @@ export class FileUpload {
this.nav_window_ref.modal_prompt.alert(e, data);
})
this.proc.done((data) => {
this.nav_window_ref.refresh();
if (!this.done_hook)
this.nav_window_ref.refresh();
})
this.proc.always(() => this?.done_hook?.());
this.reader.onerror = (evt) => {
this.modal_prompt.alert("Failed to read file: " + this.filename, "Upload of directories not supported.");
this.done();
@ -164,6 +164,7 @@ export class FileUpload {
}
this.done();
}
this.update_rates_interval = setInterval(this.display_xfr_rate.bind(this), 1000);
}
/**
@ -199,6 +200,7 @@ export class FileUpload {
done() {
this.proc.input(); // close stdin
this.remove_html_element();
clearInterval(this.update_rates_interval);
}
update_xfr_rate() {
@ -206,12 +208,19 @@ export class FileUpload {
var elapsed = (now - this.timestamp) / 1000;
this.timestamp = now;
var rate = this.chunk_size / elapsed;
this.rate.innerText = cockpit.format_bytes_per_sec(rate);
this.rate_avg = (this.rate_avg)
? (0.125 * rate + (0.875 * this.rate_avg))
: rate;
// keep exponential moving average of chunk time for eta
this.chunk_time = (this.chunk_time)
? (0.125 * elapsed + (0.875 * this.chunk_time))
: elapsed;
var eta = (this.num_chunks - this.chunk_index) * this.chunk_time;
this.eta.innerText = format_time_remaining(eta);
this.eta_avg = eta;
}
display_xfr_rate() {
this.rate.innerText = cockpit.format_bytes_per_sec(this.rate_avg);
this.eta.innerText = format_time_remaining(this.eta_avg);
}
}

View File

@ -0,0 +1,62 @@
/*
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
Copyright (C) 2021 Sam Silver <ssilver@45drives.com>
Copyright (C) 2021 Dawson Della Valle <ddellavalle@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 {FileUpload} from "./FileUpload.js";
import {NavWindow} from "./NavWindow.js";
export class FileUploadManager {
/**
*
* @param {NavWindow} nav_window_ref
* @param {number} max_concurrent
*/
constructor(nav_window_ref, max_concurrent = 10) {
this.nav_window_ref = nav_window_ref;
this.running = 0;
this.remaining_uploads = [];
this.max_concurrent = max_concurrent;
this.start_next = () => {
let next_upload = this.remaining_uploads.pop();
if (next_upload) {
next_upload?.upload?.();
this.running++;
}
}
}
/**
*
* @param {...FileUpload} uploads
*/
add(...uploads) {
let done_hook = () => {
this.running--;
this.start_next();
if (!this.running)
this.nav_window_ref.refresh();
}
for (let upload of uploads) {
upload.done_hook = done_hook;
this.remaining_uploads.unshift(upload);
}
while (this.remaining_uploads.length && this.running < this.max_concurrent) {
this.start_next();
}
}
}

View File

@ -72,7 +72,7 @@ export class NavContextMenu {
new_link(e) {
var default_target = "";
if (this.nav_window_ref.selected_entries.size <= 1 && this.target !== this.nav_window_ref.pwd())
default_target = this.target.filename();
default_target = this.target.filename;
this.nav_window_ref.ln(default_target);
}
@ -90,7 +90,14 @@ export class NavContextMenu {
async rename(e) {
this.hide();
this.target.show_edit(this.target.dom_element.nav_item_title);
if (this.target.is_dangerous_path()) {
await this.nav_window_ref.modal_prompt.alert(
"Cannot rename system-critical paths.",
"If you think you need to, use the terminal."
);
} else {
this.target.show_edit(this.target.dom_element.nav_item_title);
}
e.stopPropagation();
}

View File

@ -213,6 +213,18 @@ export class NavDir extends NavEntry {
}
super.show_properties(extra_properties);
}
style_selected() {
this.dom_element.nav_item_icon.classList.remove("fa-folder");
this.dom_element.nav_item_icon.classList.add("fa-folder-open");
super.style_selected();
}
unstyle_selected() {
this.dom_element.nav_item_icon.classList.add("fa-folder");
this.dom_element.nav_item_icon.classList.remove("fa-folder-open");
super.unstyle_selected();
}
}
export class NavDirLink extends NavDir{

View File

@ -26,7 +26,7 @@ export class NavDownloader {
*/
constructor(file) {
this.path = file.path_str();
this.filename = file.filename();
this.filename = file.filename;
this.read_size = file.stat["size"];
}

View File

@ -18,8 +18,9 @@
*/
import {FileUpload} from "./FileUpload.js";
import { ModalPrompt } from "./ModalPrompt.js";
import {ModalPrompt} from "./ModalPrompt.js";
import {NavWindow} from "./NavWindow.js";
import {FileUploadManager} from "./FileUploadManager.js";
export class NavDragDrop {
/**
@ -35,6 +36,7 @@ export class NavDragDrop {
this.drop_area = drop_area;
this.nav_window_ref = nav_window_ref;
this.modal_prompt = new ModalPrompt();
this.upload_manager = new FileUploadManager(this.nav_window_ref, 6);
}
/**
@ -82,7 +84,6 @@ export class NavDragDrop {
let path = "";
if (item) {
let new_uploads = await this.scan_files(item, path);
console.log(new_uploads);
uploads.push(... new_uploads);
} else {
reject();
@ -98,10 +99,29 @@ export class NavDragDrop {
* @returns {FileUpload[]}
*/
async handle_conflicts(uploads) {
let test_paths = [];
for (let upload of uploads)
test_paths.push(upload.path);
let proc = cockpit.spawn(
["/usr/share/cockpit/navigator/scripts/return-exists.py3", ... test_paths],
{error: "out", superuser: "try"}
);
let exist_result;
proc.done((data) => {
exist_result = JSON.parse(data);
});
proc.fail((e, data) => {
this.nav_window_ref.modal_prompt.alert(e, data);
});
try {
await proc;
} catch {
return;
}
let keepers = [];
let requests = {};
for (let upload of uploads) {
if (!await upload.check_if_exists()) {
if (!exist_result[upload.path]) {
keepers.push(upload.filename);
continue;
}
@ -147,6 +167,7 @@ export class NavDragDrop {
this.drop_area.classList.remove("drag-enter");
break;
case "drop":
this.nav_window_ref.start_load();
let uploads;
let items = e.dataTransfer.items;
e.preventDefault();
@ -162,10 +183,13 @@ export class NavDragDrop {
}
}
this.drop_area.classList.remove("drag-enter");
if (uploads.length === 0)
if (uploads.length === 0) {
this.nav_window_ref.stop_load();
break;
}
uploads = await this.handle_conflicts(uploads);
uploads.forEach((upload) => {upload.upload()});
this.nav_window_ref.stop_load();
this.upload_manager.add(... uploads);
break;
default:
this.drop_area.classList.remove("drag-enter");

View File

@ -18,7 +18,7 @@
*/
import {NavWindow} from "./NavWindow.js";
import {format_bytes, property_entry_html, format_time} from "../functions.js";
import {format_bytes, property_entry_html, format_time, check_if_exists} from "../functions.js";
export class NavEntry {
/**
@ -33,13 +33,14 @@ export class NavEntry {
this.path = path.split("/").splice(1);
else
this.path = (path.length) ? path : [""];
this.filename = this.get_filename();
this.dom_element = document.createElement("div");
this.dom_element.classList.add("nav-item");
let icon = this.dom_element.nav_item_icon = document.createElement("i");
icon.classList.add("nav-item-icon");
let title = this.dom_element.nav_item_title = document.createElement("div");
title.classList.add("nav-item-title", "no-select");
title.innerText = this.filename();
title.innerText = this.filename;
this.dom_element.appendChild(icon);
this.dom_element.appendChild(title);
let title_edit = this.dom_element.nav_item_title.editor = document.createElement("input");
@ -63,10 +64,10 @@ export class NavEntry {
this.dom_element.addEventListener("click", this);
this.dom_element.addEventListener("contextmenu", this);
}
this.is_hidden_file = this.filename().startsWith('.');
this.is_hidden_file = this.filename.startsWith('.');
if (this.is_hidden_file)
icon.style.opacity = 0.5;
this.dom_element.title = this.filename();
this.dom_element.title = this.filename;
if (nav_window_ref && nav_window_ref.item_display === "list") {
let mode = document.createElement("div");
let owner = document.createElement("div");
@ -85,6 +86,7 @@ export class NavEntry {
this.dom_element.appendChild(group);
this.dom_element.appendChild(size);
}
this.visible = true;
}
/**
@ -97,13 +99,20 @@ export class NavEntry {
if (this.nav_window_ref.selected_entries.size === 1 && this.nav_window_ref.selected_entries.has(this)) {
switch (e.target) {
case this.dom_element.nav_item_title:
this.show_edit(e.target);
this.double_click = true;
if(this.timeout)
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.double_click = false;
if (!this.is_dangerous_path())
this.show_edit(e.target);
}, 500);
e.stopPropagation();
break;
default:
break;
}
}
}
this.nav_window_ref.set_selected(this, e.shiftKey, e.ctrlKey);
this.context_menu_ref.hide();
break;
@ -127,7 +136,7 @@ export class NavEntry {
*
* @returns {string}
*/
filename() {
get_filename() {
var name = this.path[this.path.length - 1];
if (!name)
name = "/";
@ -151,10 +160,12 @@ export class NavEntry {
}
show() {
this.visible = true;
this.dom_element.style.display = "flex";
}
hide() {
this.visible = false;
this.dom_element.style.display = "none";
}
@ -255,10 +266,13 @@ export class NavEntry {
* @param {string} new_path
*/
async rename(new_name) {
if (new_name === this.filename())
if (new_name === this.filename)
return;
if (new_name.includes("/")) {
this.nav_window_ref.modal_prompt.alert("File name can't contain `/`.");
this.nav_window_ref.modal_prompt.alert(
"File name can't contain `/`.",
"If you want to move the file, right click > cut then right click > paste."
);
return;
} else if (new_name === "..") {
this.nav_window_ref.modal_prompt.alert(
@ -267,6 +281,14 @@ export class NavEntry {
);
return;
}
let new_path = [this.nav_window_ref.pwd().path_str(), new_name].join("/");
if (await check_if_exists(new_path)) {
this.nav_window_ref.modal_prompt.alert(
"Failed to rename.",
"File exists: " + new_path
);
return;
}
try {
await this.mv(new_name);
} catch(e) {
@ -285,11 +307,22 @@ export class NavEntry {
if (!element.editor)
return;
element.hide_func = () => {this.hide_edit(element)};
element.editor.onchange = element.hide_func;
element.keydown_handler = (e) => {
switch (e.keyCode) {
case 13: // enter
this.apply_edit(element);
case 27: // esc
this.hide_edit(element);
break;
default:
break;
}
};
element.editor.addEventListener("keydown", element.keydown_handler);
window.addEventListener("click", element.hide_func);
switch (element) {
case this.dom_element.nav_item_title:
element.editor.value = this.filename();
element.editor.value = this.filename;
break;
default:
element.editor.value = element.innerText;
@ -301,7 +334,7 @@ export class NavEntry {
element.editor.focus();
}
hide_edit(element) {
apply_edit(element) {
if (!element.editor)
return;
switch (element) {
@ -311,8 +344,14 @@ export class NavEntry {
default:
break;
}
}
hide_edit(element) {
if (!element.editor)
return;
element.editor.style.display = "none";
element.style.display = "inline-block";
element.editor.removeEventListener("keydown", element.keydown_handler)
window.removeEventListener("click", element.hide_func);
}
@ -323,8 +362,8 @@ export class NavEntry {
show_properties(extra_properties = "") {
var selected_name_fields = document.getElementsByClassName("nav-info-column-filename");
for (let elem of selected_name_fields) {
elem.innerHTML = this.filename();
elem.title = this.filename();
elem.innerHTML = this.filename;
elem.title = this.filename;
}
var html = "";
html += property_entry_html("Mode", this.stat["mode-str"]);
@ -339,7 +378,7 @@ export class NavEntry {
}
populate_edit_fields() {
document.getElementById("nav-edit-filename").innerText = this.filename();
document.getElementById("nav-edit-filename").innerText = this.filename;
var mode_bits = [
"other-exec", "other-write", "other-read",
"group-exec", "group-write", "group-read",
@ -353,4 +392,20 @@ export class NavEntry {
document.getElementById("nav-edit-owner").value = this.stat["owner"];
document.getElementById("nav-edit-group").value = this.stat["group"];
}
style_selected() {
this.dom_element.classList.add("nav-item-selected");
}
unstyle_selected() {
this.dom_element.classList.remove("nav-item-selected");
}
/**
*
* @returns {boolean}
*/
is_dangerous_path() {
return this.nav_window_ref.dangerous_dirs.includes(this.path_str());
}
}

View File

@ -44,6 +44,9 @@ export class NavFile extends NavEntry {
switch(e.type){
case "click":
if (this.double_click) {
if(this.timeout)
clearTimeout(this.timeout);
this.double_click = false;
this.open();
return;
} else { // single click
@ -94,7 +97,7 @@ export class NavFile extends NavEntry {
this.show_edit_file_contents();
} else {
console.log("Unknown mimetype: " + type);
if (await this.nav_window_ref.modal_prompt.confirm("Can't open " + this.filename() + " for editing.", "Download it instead?")) {
if (await this.nav_window_ref.modal_prompt.confirm("Can't open " + this.filename + " for editing.", "Download it instead?")) {
var download = new NavDownloader(this);
download.download();
}
@ -118,7 +121,7 @@ export class NavFile extends NavEntry {
document.getElementById("nav-cancel-edit-contents-btn").onclick = this.hide_edit_file_contents.bind(this);
document.getElementById("nav-continue-edit-contents-btn").onclick = this.write_to_file.bind(this);
document.getElementById("nav-edit-contents-header").innerText = "Editing " + this.path_str();
document.getElementById("nav-contents-view").style.display = "none";
document.getElementById("nav-contents-view-holder").style.display = "none";
document.getElementById("nav-edit-contents-view").style.display = "flex";
}
@ -140,7 +143,7 @@ export class NavFile extends NavEntry {
window.addEventListener("keydown", this.nav_window_ref);
document.getElementById("nav-edit-contents-textarea").removeEventListener("keydown", this);
document.getElementById("nav-edit-contents-view").style.display = "none";
document.getElementById("nav-contents-view").style.display = "flex";
document.getElementById("nav-contents-view-holder").style.display = "flex";
this.nav_window_ref.enable_buttons();
}
}
@ -195,7 +198,7 @@ export class NavFileLink extends NavFile{
this.show_edit_file_contents();
} else {
console.log("Unknown mimetype: " + type);
this.nav_window_ref.modal_prompt.alert("Can't open " + this.filename() + " for editing.");
this.nav_window_ref.modal_prompt.alert("Can't open " + this.filename + " for editing.");
}
}
@ -218,7 +221,7 @@ export class NavFileLink extends NavFile{
document.getElementById("nav-cancel-edit-contents-btn").onclick = this.hide_edit_file_contents.bind(this);
document.getElementById("nav-continue-edit-contents-btn").onclick = this.write_to_file.bind(this);
document.getElementById("nav-edit-contents-header").innerHTML = "Editing " + this.path_str() + ' <i class="fas fa-long-arrow-alt-right"></i> ' + this.get_link_target_path();
document.getElementById("nav-contents-view").style.display = "none";
document.getElementById("nav-contents-view-holder").style.display = "none";
document.getElementById("nav-edit-contents-view").style.display = "flex";
}

View File

@ -38,7 +38,6 @@ export class NavWindow {
this.window.addEventListener("click", this);
this.window.addEventListener("contextmenu", this);
window.addEventListener("keydown", this);
this.last_selected_index = -1;
this.context_menu = new NavContextMenu("nav-context-menu", this);
@ -49,6 +48,36 @@ export class NavWindow {
this.sort_function = new SortFunctions();
this.modal_prompt = new ModalPrompt();
this.dangerous_dirs = [
"/",
"/bin",
"/boot",
"/dev",
"/etc",
"/home",
"/lib",
"/lib32",
"/lib64",
"/mnt",
"/opt",
"/proc",
"/root",
"/run",
"/sbin",
"/srv",
"/sys",
"/tmp",
"/usr",
"/usr/bin",
"/usr/include",
"/usr/lib",
"/usr/lib32",
"/usr/lib64",
"/usr/sbin",
"/usr/share",
"/var"
];
}
/**
@ -59,7 +88,7 @@ export class NavWindow {
switch (e.type) {
case "click":
if (e.target === this.window) {
this.clear_selected();
this.reset_selection();
this.show_selected_properties();
}
break;
@ -73,6 +102,8 @@ export class NavWindow {
} else if (e.keyCode === 65 && e.ctrlKey) {
this.select_all();
e.preventDefault();
} else if (e.keyCode === 27) {
this.reset_selection();
} else if (e.keyCode === 67 && e.ctrlKey) {
this.copy();
} else if (e.keyCode === 86 && e.ctrlKey) {
@ -129,7 +160,7 @@ export class NavWindow {
file.context_menu_ref = this.context_menu;
});
document.getElementById("pwd").value = this.pwd().path_str();
this.set_selected(this.pwd(), false, false);
this.reset_selection();
this.show_selected_properties();
document.getElementById("nav-num-dirs").innerText = `${num_dirs} Director${(num_dirs === 1)? "y" : "ies"}`;
document.getElementById("nav-num-files").innerText = `${num_files} File${(num_files === 1)? "" : "s"}`;
@ -178,65 +209,85 @@ export class NavWindow {
this.cd(new NavDir(this.pwd().parent_dir()));
}
clear_selected() {
for (let entry of this.selected_entries) {
entry.unstyle_selected();
}
this.selected_entries.clear();
}
/**
*
* @param {NavEntry} entry
* @param {Boolean} select_range
* @param {Boolean} append
*/
set_selected(entry, select_range, append) {
this.hide_edit_selected();
for (let i of this.selected_entries) {
i.dom_element.classList.remove("nav-item-selected");
if (i.nav_type === "dir") {
i.dom_element.nav_item_icon.classList.remove("fa-folder-open");
i.dom_element.nav_item_icon.classList.add("fa-folder");
}
}
var to_be_selected = [];
if (append && this.selected_entries.has(entry)) {
this.selected_entries.delete(entry);
if (this.selected_entries.size === 0) {
this.clear_selected();
}
} else if (select_range && this.last_selected_index !== -1) {
var start = this.last_selected_index;
var end = this.entries.indexOf(entry);
if (end < start)
[start, end] = [end, start];
if (end === -1)
return;
to_be_selected = this.entries.slice(start, end + 1).filter(entry => !entry.stat["inaccessible"]);
} else {
if (!append)
this.selected_entries.clear();
to_be_selected = [entry];
}
for (let i of to_be_selected) {
this.selected_entries.add(i);
}
for (let i of this.selected_entries) {
i.dom_element.classList.add("nav-item-selected");
if (i.nav_type === "dir") {
i.dom_element.nav_item_icon.classList.remove("fa-folder");
i.dom_element.nav_item_icon.classList.add("fa-folder-open");
}
}
if (this.selected_entries.size > 1){
var name_fields = document.getElementsByClassName("nav-info-column-filename");
for (let name_field of name_fields) {
name_field.innerText = this.selected_entries.size.toString() + " selected"
name_field.title = name_field.innerText;
}
document.getElementById("nav-info-column-properties").innerHTML = "";
} else {
this.show_selected_properties();
}
this.last_selected_index = this.entries.indexOf(entry);
select_one(entry) {
entry.style_selected();
this.selected_entries.add(entry);
}
clear_selected() {
this.set_selected(this.pwd(), false, false);
deselect_one(entry) {
entry.unstyle_selected();
this.selected_entries.delete(entry);
}
/**
*
* @param {NavEntry} start
* @param {NavEntry} end
*/
select_range(start, end) {
let start_ind = this.entries.indexOf(start);
let end_ind = this.entries.indexOf(end);
if (start_ind === -1 || end_ind === -1)
return;
if (end_ind === start_ind)
this.select_one(start);
else if (end_ind < start_ind)
[start_ind, end_ind] = [end_ind, start_ind];
for (let i = start_ind; i <= end_ind; i++) {
let entry = this.entries[i];
if (entry.visible && (!entry.is_hidden_file || this.show_hidden))
this.select_one(entry);
}
}
reset_selection() {
this.clear_selected();
this.select_one(this.pwd());
this.last_selected_entry = null;
this.update_selection_info();
}
/**
*
* @param {NavEntry} target
* @param {Boolean} shift
* @param {Boolean} ctrl
*/
set_selected(target, shift, ctrl) {
if (!ctrl && !shift)
this.clear_selected();
if (!shift || !this.last_selected_entry)
this.last_selected_entry = target;
if (shift) {
if (!ctrl)
this.clear_selected();
this.select_range(
this.last_selected_entry ?? this.entries[0],
target
);
} else if (ctrl && this.selected_entries.has(target)) {
this.deselect_one(target)
} else {
this.select_one(target);
}
this.update_selection_info();
}
select_all() {
this.clear_selected();
this.select_range(this.entries[0], this.entries[this.entries.length - 1]);
this.update_selection_info();
}
selected_entry() {
@ -248,29 +299,27 @@ export class NavWindow {
}
show_selected_properties() {
this.selected_entry().show_properties();
this.selected_entry()?.show_properties?.();
}
update_selection_info() {
if (this.selected_entries.size > 1){
var name_fields = document.getElementsByClassName("nav-info-column-filename");
for (let name_field of name_fields) {
name_field.innerText = this.selected_entries.size.toString() + " selected"
name_field.title = name_field.innerText;
}
document.getElementById("nav-info-column-properties").innerHTML = "";
} else {
this.show_selected_properties();
}
}
async show_edit_selected() {
var dangerous_dirs = [
"/",
"/usr",
"/bin",
"/sbin",
"/lib",
"/lib32",
"/lib64",
"/usr/bin",
"/usr/include",
"/usr/lib",
"/usr/lib32",
"/usr/lib64",
"/usr/sbin",
];
var dangerous_selected = [];
for (let i of this.selected_entries) {
var path = i.path_str();
if (dangerous_dirs.includes(path)) {
if (this.dangerous_dirs.includes(path)) {
dangerous_selected.push(path);
}
}
@ -321,7 +370,7 @@ export class NavWindow {
}
var targets = [];
for (let target of this.selected_entries) {
targets.push(target.filename());
targets.push(target.filename);
}
var targets_str = targets.join(", ");
document.getElementById("selected-files-list-header").innerText = "Applying edits to:";
@ -395,6 +444,8 @@ export class NavWindow {
}
async delete_selected() {
if (await this.check_if_dangerous("delete"))
return;
var prompt = "";
if (this.selected_entries.size > 1) {
prompt = "Deleting " + this.selected_entries.size + " files.";
@ -404,6 +455,7 @@ export class NavWindow {
if (!await this.modal_prompt.confirm(prompt, "This cannot be undone. Are you sure?", true)) {
return;
}
this.start_load();
for (let target of this.selected_entries) {
try {
await target.rm();
@ -411,6 +463,7 @@ export class NavWindow {
this.modal_prompt.alert(e);
}
}
this.stop_load();
this.refresh();
}
@ -545,7 +598,9 @@ export class NavWindow {
this.refresh();
}
cut() {
async cut() {
if (await this.check_if_dangerous("move"))
return;
this.clip_board = [...this.selected_entries];
this.copy_or_move = "move";
this.paste_cwd = this.pwd().path_str();
@ -643,6 +698,7 @@ export class NavWindow {
default:
break;
}
e.stopPropagation();
}
nav_bar_cd() {
@ -681,7 +737,7 @@ export class NavWindow {
start_load() {
document.getElementById("nav-loader-container").style.display = "block";
var buttons = document.getElementsByTagName("button");
var buttons = document.getElementsByClassName("disable-while-loading");
for (let button of buttons) {
button.disabled = true;
}
@ -689,7 +745,7 @@ export class NavWindow {
stop_load() {
document.getElementById("nav-loader-container").style.display = "none";
var buttons = document.getElementsByTagName("button");
var buttons = document.getElementsByClassName("disable-while-loading");
for (let button of buttons) {
button.disabled = false;
}
@ -801,15 +857,6 @@ export class NavWindow {
this.set_nav_button_state();
}
select_all() {
this.selected_entries.clear();
for (let entry of this.entries) {
if (!entry.is_hidden_file || this.show_hidden) {
this.set_selected(entry, false, true);
}
}
}
async switch_item_display() {
var button = document.getElementById("nav-item-display-icon");
if (this.item_display === "grid") {
@ -833,11 +880,42 @@ export class NavWindow {
search_filter(event) {
var search_name = event.target.value;
let search_func;
if (search_name[0] === '*')
search_func = (entry) => entry.filename.toLowerCase().includes(search_name.slice(1).toLowerCase());
else
search_func = (entry) => entry.filename.toLowerCase().startsWith(search_name.toLowerCase());
this.entries.forEach((entry) => {
if (entry.filename().toLowerCase().startsWith(search_name.toLowerCase()))
if (search_func(entry))
entry.show();
else
entry.hide();
});
}
/**
*
* @param {string} verb
* @returns {Promise<boolean>}
*/
check_if_dangerous(verb) {
return new Promise(async (resolve, reject) => {
let dangerous_selected = [];
for (let entry of this.selected_entries) {
let path = entry.path_str();
if (this.dangerous_dirs.includes(path)) {
dangerous_selected.push(path);
}
}
if (dangerous_selected.length) {
await this.modal_prompt.alert(
`Cannot ${verb} system-critical paths.`,
`The following path(s) are very dangerous to ${verb}: ${dangerous_selected.join(", ")}. If you think you need to ${verb} them, use the terminal.`
);
resolve(true);
} else {
resolve(false);
}
});
}
}

View File

@ -60,11 +60,11 @@ export class SortFunctions {
}
name_asc(first, second) {
return first.filename().localeCompare(second.filename());
return first.filename.localeCompare(second.filename);
}
name_desc(first, second) {
return second.filename().localeCompare(first.filename());
return second.filename.localeCompare(first.filename);
}
owner_asc(first, second) {

View File

@ -70,9 +70,7 @@ export function format_time_remaining(seconds_) {
if (hours) {
out = String(hours).padStart(2, '0') + ":";
}
if (minutes) {
out += String(minutes).padStart(2, '0') + ":";
}
out += String(minutes).padStart(2, '0') + ":";
out += String(seconds).padStart(2, '0');
return out;
}
@ -95,4 +93,17 @@ export function format_permissions(mode) {
}
}
return result;
}
}
/**
*
* @param {string} path
* @returns {Promise<boolean>}
*/
export function check_if_exists(path) {
return new Promise((resolve, reject) => {
var proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/fail-if-exists.py3", path], {superuser: "try"});
proc.done((data) => {resolve(false)});
proc.fail((e, data) => {resolve(true)});
});
}

View File

@ -651,7 +651,7 @@ input:checked + .slider:before {
.nav-notifications {
position: absolute;
bottom: 0;
right: 0;
right: 10px;
padding: 5px;
display: flex;
flex-flow: column-reverse nowrap;
@ -663,7 +663,7 @@ input:checked + .slider:before {
.nav-notification {
margin: 5px;
position: relative;
display: flex;
display: none;
flex-flow: column nowrap;
align-items: stretch;
z-index: 10;
@ -673,15 +673,23 @@ input:checked + .slider:before {
border-radius: var(--nav-border-radius);
color: var(--font);
}
.nav-notification-header {
/* .nav-notification-header {
position: relative;
z-index: 10;
font-weight: bold;
}
} */
.nav-notification-header > progress {
/* .nav-notification-header > progress {
position: relative;
z-index: 10;
} */
.nav-notification-header > p {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-border {
border: none;
}

View File

@ -40,13 +40,13 @@
<div class="flex-col outer-container">
<div class="flex-row nav-header">
<div class="nav-btn-group">
<button class="pf-c-button pf-m-secondary" id="nav-back-btn" title="Back"><i class="fas fa-arrow-left"></i></button>
<button class="disable-while-loading pf-c-button pf-m-secondary" id="nav-back-btn" title="Back"><i class="fas fa-arrow-left"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-secondary" id="nav-forward-btn" title="Forward"><i class="fas fa-arrow-right"></i></button>
<button class="disable-while-loading pf-c-button pf-m-secondary" id="nav-forward-btn" title="Forward"><i class="fas fa-arrow-right"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-secondary" id="nav-up-dir-btn" title="Up"><i class="fas fa-arrow-up"></i></button>
<button class="disable-while-loading pf-c-button pf-m-secondary" id="nav-up-dir-btn" title="Up"><i class="fas fa-arrow-up"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-secondary" id="nav-refresh-btn" title="Refresh"><i class="fas fa-sync"></i></button>
<button class="disable-while-loading pf-c-button pf-m-secondary" id="nav-refresh-btn" title="Refresh"><i class="fas fa-sync"></i></button>
</div>
<div class="horizontal-spacer"></div>
<input type="text" list="possible-paths-list" autocomplete="off" class="navigation-bar" id="pwd" title="Navigation Bar"></input>
@ -55,27 +55,29 @@
</select>
</datalist>
<div class="horizontal-spacer"></div>
<input type="text" autocomplete="off" class="search-bar" id="search-bar" title="Search in Directory" placeholder="Search in Directory"></input>
<input type="text" autocomplete="off" class="search-bar" id="search-bar" title="Prepend * to fuzzy search" placeholder="Search in Directory"></input>
<i class="fas fa-search"></i>
<div class="horizontal-spacer"></div>
<div class="nav-btn-group">
<button class="pf-c-button pf-m-primary" id="nav-mkdir-btn" title="New Directory"><i class="fas fa-folder-plus"></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary" id="nav-mkdir-btn" title="New Directory"><i class="fas fa-folder-plus"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary" id="nav-touch-btn" title="New File"><i class="fas fa-file-medical"></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary" id="nav-touch-btn" title="New File"><i class="fas fa-file-medical"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary" id="nav-ln-btn" title="New Symbolic Link"><i class="fas fa-link nav-icon-decorated"><i class="fas fa-plus nav-icon-decoration"></i></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary" id="nav-ln-btn" title="New Symbolic Link"><i class="fas fa-link nav-icon-decorated"><i class="fas fa-plus nav-icon-decoration"></i></i></button>
</div>
</div>
<div class="vertical-spacer"></div>
<div class="flex-row inner-container">
<div class="contents-view contents-view-grid" id="nav-contents-view">
<div class="contents-view-list-header nav-item">
<i class="nav-item-icon"></i>
<div class="nav-item-title" id="sort-name-btn">Name<i class="sort-arrow fas fa-chevron-up" id="sort-name-icon"></i></div>
<div id="sort-mode-btn">Mode</div>
<div id="sort-owner-btn">Owner<i class="sort-arrow fas" id="sort-owner-icon"></i></div>
<div id="sort-group-btn">Group<i class="sort-arrow fas" id="sort-group-icon"></i></div>
<div id="sort-size-btn">Size<i class="sort-arrow fas" id="sort-size-icon"></i></div>
<div class="contents-view" id="nav-contents-view-holder">
<div class="contents-view contents-view-grid no-border" id="nav-contents-view">
<div class="contents-view-list-header nav-item">
<i class="nav-item-icon"></i>
<div class="nav-item-title" id="sort-name-btn">Name<i class="sort-arrow fas fa-chevron-up" id="sort-name-icon"></i></div>
<div id="sort-mode-btn">Mode</div>
<div id="sort-owner-btn">Owner<i class="sort-arrow fas" id="sort-owner-icon"></i></div>
<div id="sort-group-btn">Group<i class="sort-arrow fas" id="sort-group-icon"></i></div>
<div id="sort-size-btn">Size<i class="sort-arrow fas" id="sort-size-icon"></i></div>
</div>
</div>
<div class="nav-notifications" id="nav-notifications">
</div>
@ -86,9 +88,9 @@
<textarea id="nav-edit-contents-textarea" spellcheck="false"></textarea>
<div class="vertical-spacer"></div>
<div class="nav-btn-group">
<button class="pf-c-button pf-m-danger editor-btn" id="nav-cancel-edit-contents-btn" title="Cancel"><i class="fas fa-times"></i></button>
<button class="disable-while-loading pf-c-button pf-m-danger editor-btn" id="nav-cancel-edit-contents-btn" title="Cancel"><i class="fas fa-times"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary editor-btn" id="nav-continue-edit-contents-btn" title="Save"><i class="fas fa-save"></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary editor-btn" id="nav-continue-edit-contents-btn" title="Save"><i class="fas fa-save"></i></button>
</div>
</div>
<div class="horizontal-spacer"></div>
@ -97,9 +99,9 @@
<div class="flex-row space-between">
<div class="nav-info-column-filename"></div>
<div class="nav-btn-group">
<button class="pf-c-button pf-m-danger" id="nav-delete-btn" title="Delete"><i class="fas fa-trash-alt"></i></button>
<button class="disable-while-loading pf-c-button pf-m-danger" id="nav-delete-btn" title="Delete"><i class="fas fa-trash-alt"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary" id="nav-edit-properties-btn" title="Edit Properties"><i class="fas fa-sliders-h"></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary" id="nav-edit-properties-btn" title="Edit Properties"><i class="fas fa-sliders-h"></i></button>
</div>
</div>
<div class="nav-info-column-properties" id="nav-info-column-properties"></div>
@ -153,9 +155,9 @@
<div class="flex-grow monospace-sm" id="selected-files-list"></div>
<div class="vertical-spacer"></div>
<div class="nav-btn-group">
<button class="pf-c-button pf-m-danger" id="nav-cancel-edit-btn" title="Cancel"><i class="fas fa-times"></i></button>
<button class="disable-while-loading pf-c-button pf-m-danger" id="nav-cancel-edit-btn" title="Cancel"><i class="fas fa-times"></i></button>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary" id="nav-apply-edit-btn" title="Save Changes"><i class="fas fa-save"></i></button>
<button class="disable-while-loading pf-c-button pf-m-primary" id="nav-apply-edit-btn" title="Save Changes"><i class="fas fa-save"></i></button>
</div>
</div>
</div>

View File

@ -130,6 +130,7 @@ function set_up_buttons() {
document.getElementById("search-bar").addEventListener("keydown", (e) => {
if (e.keyCode === 13)
nav_window.search_filter(e);
e.stopPropagation();
});
// fix tab in editor input
document.getElementById('nav-edit-contents-textarea').addEventListener('keydown', (e) => {

View File

@ -0,0 +1,44 @@
#!/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/>.
"""
"""
Synopsys: return-exists.py3 /full/path1 [/full/path2 ...]
replys with JSON formatted dictionary of path : boolean where true means the file exists
"""
import os
import sys
import json
def main():
argv = sys.argv
argc = len(sys.argv)
if argc <= 1:
print("No arguments provided")
sys.exit(1)
response = {}
for i in range (1, argc):
path = argv[i]
response[path] = os.path.lexists(path)
print(json.dumps(response))
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -18,7 +18,7 @@
"""
"""
Synopsis: `write-chunks.py3 <newline delimited JSON objects>`
Synopsis: `echo <newline delimited JSON objects> | write-chunks.py3`
JSON objects are of form:
obj = {
seek: <byte offset>
@ -35,20 +35,24 @@ def write_chunk(chunk, file):
if not file:
path = sys.argv[1]
parent_path = os.path.dirname(path)
if not os.path.exists(parent_path):
os.makedirs(parent_path, exist_ok=True)
elif os.path.isfile(parent_path):
print(parent_path + ": exists and is not a directory.")
sys.exit(1)
try:
if not os.path.exists(parent_path):
os.makedirs(parent_path, exist_ok=True)
elif os.path.isfile(parent_path):
print(parent_path + ": exists and is not a directory.")
sys.exit(1)
file = open(path, "wb")
except Exception as e:
print(e)
sys.exit(1)
seek = chunk["seek"]
data = base64.b64decode(chunk["chunk"])
file.seek(seek)
file.write(data)
try:
file.seek(seek)
file.write(data)
except Exception as e:
print(e)
sys.exit(1)
def main():
file = None

View File

@ -32,6 +32,11 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/*
%changelog
* Tue Jul 20 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.4-1
- Add fuzzy search.
- Optimize folder uploads.
- Fix bugs with selecting entries and renaming files.
- Stop user from deleting or renaming system-critical paths.
* Mon Jul 19 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.3-1
- Implement inline filename editing.
- Add information popup button.

View File

@ -32,6 +32,11 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/*
%changelog
* Tue Jul 20 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.4-1
- Add fuzzy search.
- Optimize folder uploads.
- Fix bugs with selecting entries and renaming files.
- Stop user from deleting or renaming system-critical paths.
* Mon Jul 19 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.3-1
- Implement inline filename editing.
- Add information popup button.

View File

@ -1,3 +1,12 @@
cockpit-navigator (0.5.4-1focal) focal; urgency=medium
* Add fuzzy search.
* Optimize folder uploads.
* Fix bugs with selecting entries and renaming files.
* Stop user from deleting or renaming system-critical paths.
-- Josh Boudreau <jboudreau@45drives.com> Tue, 20 Jul 2021 13:06:32 -0300
cockpit-navigator (0.5.3-1focal) focal; urgency=medium
* Implement inline filename editing.