Merge branch 'dev-josh'

This commit is contained in:
joshuaboud 2021-07-15 15:57:21 -03:00
commit 7862b187d5
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
29 changed files with 2640 additions and 2061 deletions

View File

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

View File

@ -1,3 +1,3 @@
## Cockpit Navigator 0.4.6-3
## Cockpit Navigator 0.5.0-1
* Add relase for el7
* Implement custom modal style popups to replace browser dialogs.

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.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 <version>` (v0.4.6 is latest)
1. `$ git checkout <version>` (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

View File

@ -13,6 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>.
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

View File

@ -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 <jboudreau@45drives.com>",
"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 <jboudreau@45drives.com>",

View File

@ -0,0 +1,185 @@
/*
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 {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);
}
}

View File

@ -0,0 +1,230 @@
/*
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/>.
*/
/**
* @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<boolean>}
*/
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.<string, Request>} requests
* @returns {Promise<Object|string>}
*/
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();
}
});
}
}

View File

@ -0,0 +1,215 @@
/*
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 {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", '<div><i class="fas fa-folder-plus"></i></div>'],
["new_file", '<div><i class="fas fa-file-medical"></i></div>'],
["new_link", '<div><i class="fas fa-link nav-icon-decorated"><i class="fas fa-plus nav-icon-decoration"></i></i></div>'],
["cut", '<div><i class="fas fa-cut"></i></div>'],
["copy", '<div><i class="fas fa-copy"></i></div>'],
["paste", '<div><i class="fas fa-paste"></i></div>'],
["rename", '<div><i class="fas fa-i-cursor"></i></div>'],
["delete", '<div><i class="fas fa-trash-alt"></i></div>'],
["download", '<div><i class="fas fa-download"></i></div>'],
["properties", '<div><i class="fas fa-sliders-h"></i></div>']
];
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";
}
}

View File

@ -0,0 +1,261 @@
/*
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 {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<NavEntry[]>}
*/
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<void>}
*/
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 +=
'<div class="vertical-spacer"></div><h2 class="nav-info-column-filename">Ceph Status</h2>';
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 += " &#8594; " + this.link_target;
}
/**
*
* @returns {Promise<void>}
*/
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);
}
}

View File

@ -0,0 +1,58 @@
/*
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 {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);
}
}

View File

@ -0,0 +1,91 @@
/*
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 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();
}
}

View File

@ -0,0 +1,267 @@
/*
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 {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<void>}
*/
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<void>}
*/
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<void>}
*/
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"];
}
}

View File

@ -0,0 +1,235 @@
/*
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 {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<void>}
*/
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 += " &#8594; " + 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() + ' <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-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();
}
}

View File

@ -0,0 +1,841 @@
/*
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 {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<void>}
*/
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<void>}
*/
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();
});
}
}

View File

@ -0,0 +1,93 @@
/*
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/>.
*/
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"];
}
}

98
navigator/functions.js Normal file
View File

@ -0,0 +1,98 @@
/*
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/>.
*/
/**
*
* @param {string} key
* @param {string} value
* @returns {string}
*/
export function property_entry_html(key, value) {
var html = '<div class="nav-property-pair">';
html += '<span class="nav-property-pair-key">' + key + '</span>';
html += '<span class="nav-property-pair-value">' + value + '</span>';
html += '</div>';
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;
}

View File

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

View File

@ -20,6 +20,7 @@
<head>
<title>Navigator</title>
<meta charset="utf-8">
<base href=".">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link href="../base1/cockpit.css" type="text/css" rel="stylesheet">
<link href="navigator.css" type="text/css" rel="stylesheet">
@ -28,7 +29,7 @@
<script src="../base1/cockpit.js"></script>
<script src="../manifests.js"></script>
<script src="../*/po.js"></script>
<script defer src="navigator.js"></script>
<script src="navigator.js" type="module"></script>
</head>
<body>
<div class="nav-loader-container" id="nav-loader-container">

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"""
"""
Synopsis: `paste.py [-m] <cwd of copy> <list of source files> <destination directory>`
Synopsis: `paste.py3 [-m] <cwd of copy> <list of source files> <destination directory>`
all full paths
"""
@ -28,15 +28,19 @@ from optparse import OptionParser
import json
import subprocess
def prompt_user(message, wants_response):
def prompt_user(message, wants_response, conflicts = None):
payload = {
"wants-response": wants_response,
"message": message
}
if conflicts != None:
payload["conflicts"] = conflicts
print(json.dumps(payload) + "\n")
if wants_response:
response = input()
return json.loads(response)
response = json.loads(input())
if isinstance(response, str) and response == "abort":
sys.exit(0)
return response
return
def split_paths_at_cwd(paths, cwd):
@ -70,12 +74,8 @@ def filter_existing(args, cwd):
dest = args[-1]
(conflicts, non_conflicts) = recursive_get_conflicts(sources, cwd, dest)
if len(conflicts):
check_continue = prompt_user("Conflicts were found while pasting. `Cancel` to abort operation, `OK` to overwrite selectively.", True)
if not check_continue:
sys.exit(0)
for conflict in conflicts:
if prompt_user("Overwrite " + conflict[1] + "?", True):
non_conflicts.append(conflict[0])
conflicts = prompt_user("Overwrite?", True, conflicts)
non_conflicts.extend(conflicts)
if not len(non_conflicts):
sys.exit(0) # exit if nothing to copy
filtered_args = [*split_paths_at_cwd(non_conflicts, cwd), dest]

View File

@ -18,7 +18,7 @@
"""
"""
Synopsis: `write-chunks.py <newline delimited JSON objects>`
Synopsis: `write-chunks.py3 <newline delimited JSON objects>`
JSON objects are of form:
obj = {
seek: <byte offset>

View File

@ -18,7 +18,7 @@
"""
"""
Synopsis: `zip-for-download.py </path/to/cwd> </path/to/file> [</path/to/file> ...]`
Synopsis: `zip-for-download.py3 </path/to/cwd> </path/to/file> [</path/to/file> ...]`
Output is JSON object with form:
{
message: <error message if applicable>,

View File

@ -18,7 +18,6 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
%prep
%setup -q
alias python=python3
%build
# empty
@ -33,6 +32,8 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/*
%changelog
* Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.0-1
- Implement custom modal style popups to replace browser dialogs.
* Wed Jul 07 2021 Josh Boudreau <jboudreau@45drives.com> 0.4.6-3
- Add relase for el7
* Wed Jun 30 2021 Josh Boudreau <jboudreau@45drives.com> 0.4.6-2

View File

@ -32,6 +32,8 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/*
%changelog
* Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.0-1
- Implement custom modal style popups to replace browser dialogs.
* Wed Jul 07 2021 Josh Boudreau <jboudreau@45drives.com> 0.4.6-3
- Add relase for el7
* Wed Jun 30 2021 Josh Boudreau <jboudreau@45drives.com> 0.4.6-2

View File

@ -1,3 +1,9 @@
cockpit-navigator (0.5.0-1focal) focal; urgency=medium
* Implement custom modal style popups to replace browser dialogs.
-- Josh Boudreau <jboudreau@45drives.com> Thu, 15 Jul 2021 10:42:41 -0300
cockpit-navigator (0.4.6-3focal) focal; urgency=medium
* Add relase for el7