Merge branch 'main' into dev-sam

This commit is contained in:
sam55silver 2021-05-28 10:15:01 -03:00
commit 93721b8dec
9 changed files with 923 additions and 217 deletions

View File

@ -1,2 +1,2 @@
# cockpit-navigator
File Browser for Cockpit
# Cockpit Navigator
A File System Browser for Cockpit.

View File

@ -1,3 +1,18 @@
# 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/>.
default:

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,3 +1,21 @@
/*
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
Copyright (C) 2021 Sam Silver <ssilver@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/>.
*/
:root {
/* white style */
--container: #fff;
@ -7,6 +25,12 @@
--selected: #fff;
--toggle-light: #151515;
--toggle-dark: #ccc;
--scrollbar-thumb: var(--border);
--scrollbar-bg: var(--navigation);
--scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg);
--loading-bg-color: rgba(255, 255, 255, 0.5);
--textarea-bg: var(--navigation);
--logo-45: #333;
}
[data-theme="dark"] {
@ -15,9 +39,81 @@
--border: #3c3f42;
--font: #fff;
--selected: #191a1b;
--navigation: #121212;
--scrollbar-thumb: var(--container);
--scrollbar-bg: var(--navigation);
--scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg);
--loading-bg-color: rgba(33, 36, 39, 0.5);
--textarea-bg: var(--navigation);
--logo-45: #fff;
}
.pf-c-button:disabled[data-theme="dark"] {
background-color: var(--border);
}
.nav-loader-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: var(--loading-bg-color);
z-index: 10;
}
.nav-loader-centerer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 27%;
display: flex;
align-items: center;
align-content: center;
}
.nav-loader {
margin: auto;
border: 6px solid rgba(0,0,0,0);
border-radius: 50%;
border-top: 6px solid var(--border);
width: 100px;
height: 100px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;
}
/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
body::-webkit-scrollbar {
width: 11px;
}
body {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-color);
}
body::-webkit-scrollbar-track {
background: var(--scrollbar-bg);
}
body::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb) ;
border-radius: 6px;
border: 3px solid var(--scrollbar-bg);
}
.flex-row {
display: flex;
flex-direction: row;
@ -36,10 +132,18 @@
margin-right: 1em;
}
.horizontal-spacer-sm {
margin-right: 0.25em;
}
.vertical-spacer {
margin-bottom: 1em;
}
.spacer-stretchy {
flex-grow: 1;
}
.nav-hidden {
display: none;
}
@ -49,10 +153,12 @@
background-color: var(--container);
color: var(--font);
padding: 1em;
padding-bottom: 0;
}
.navigation-bar {
background-color: var(--container);
color: inherit;
flex-grow: 1;
padding: 0.25em 1em 0.25em 1em;
border: 1px solid var(--border);
@ -166,18 +272,63 @@
align-items: flex-start;
}
.editor-header {
font-weight: bold;
}
.edit-file-contents {
height: 100%;
flex-basis: 0;
flex-grow: 8;
flex-flow: column nowrap;
align-items: stretch;
}
.edit-file-contents > textarea {
flex-grow: 1;
white-space: pre;
overflow: auto;
resize: none;
border: 1px solid var(--border);
border-radius: 4px;
outline: none;
padding: 5px;
color: var(--font);
background-color: var(--textarea-bg);
}
.nav-footer {
flex: 1;
align-items: baseline;
justify-content: space-between;
padding: 5px;
}
.nav-footer > a > img {
height: 1.25em;
width: auto;
margin-bottom: 4px;
}
.nav-footer > a > .logo-45 {
font-weight: 900;
color: var(--logo-45);
}
.nav-footer > a > .logo-drives {
font-weight: 600;
color: #981c20;
}
.nav-toggle {
position: absolute;
right: 0;
bottom: 0.5em;
margin-right: 1.9em;
justify-self: flex-end;
}
.switch {
position: relative;
display: inline-block;
width: 38px;
height: 20px;
width: 30px;
height: 17px;
}
.switch input {
@ -201,8 +352,8 @@
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
@ -219,13 +370,13 @@ input:focus + .slider {
}
input:checked + .slider:before {
-webkit-transform: translateX(18px);
-ms-transform: translateX(18px);
transform: translateX(18px);
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
.slider.round {
border-radius: 34px;
border-radius: 17px;
}
.slider.round:before {

View File

@ -1,20 +1,20 @@
<!doctype html>
<html>
<!--
Cockpit Samba Manager - Cockpit plugin for managing Samba.
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
Copyright (C) 2021 Sam Silver <ssilver@45drives.com>
This file is part of Cockpit Samba Manager.
Cockpit Samba Manager is free software: you can redistribute it and/or modify
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 Samba Manager is distributed in the hope that it will be useful,
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 Samba Manager. If not, see <https://www.gnu.org/licenses/>.
along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>.
-->
<html lang="en">
<head>
@ -30,17 +30,28 @@
<script defer src="navigator.js"></script>
</head>
<body>
<div class="nav-loader-container" id="nav-loader-container">
<div class="nav-loader-centerer">
<div class="nav-loader"></div>
</div>
</div>
<div class="flex-col outer-container">
<div class="flex-row">
<div class="nav-btn-group">
<button class="pf-c-button pf-m-secondary" id="nav-back-btn"><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"><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"><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"><i class="fas fa-sync"></i></button>
</div>
<div class="horizontal-spacer"></div>
<div class="navigation-bar" id="pwd">
/current/dir
</div>
<input type="text" list="possible-paths-list" autocomplete="off" class="navigation-bar" id="pwd"></input>
<datalist id="possible-paths-list">
<select id="possible-paths">
</select>
</datalist>
<div class="horizontal-spacer"></div>
<button class="pf-c-button pf-m-primary" id="nav-mkdir-btn"><i class="fas fa-folder-plus"></i></button>
<div class="horizontal-spacer"></div>
@ -49,6 +60,17 @@
<div class="vertical-spacer"></div>
<div class="flex-row inner-container">
<div class="contents-view" id="nav-contents-view"></div>
<div class="edit-file-contents nav-hidden" id="nav-edit-contents-view">
<div class="editor-header" id="nav-edit-contents-header"></div>
<div class="vertical-spacer"></div>
<textarea id="nav-edit-contents-textarea"></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"><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"><i class="fas fa-save"></i></button>
</div>
</div>
<div class="horizontal-spacer"></div>
<div class="nav-info-column" id="nav-info-column">
<div id="nav-show-properties">
@ -57,17 +79,10 @@
<div class="nav-btn-group">
<button class="pf-c-button pf-m-danger" id="nav-delete-btn"><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"><i class="fas fa-edit"></i></button>
<button class="pf-c-button pf-m-primary" id="nav-edit-properties-btn"><i class="fas fa-sliders-h"></i></button>
</div>
</div>
<div class="nav-info-column-properties" id="nav-info-column-properties"></div>
<div class="nav-toggle">
<label class="switch">
<input type="checkbox" id="toggle-theme">
<span class="slider round"></span>
</label>
<div class="vertical-spacer"></div>
</div>
</div>
<div class="nav-hidden" id="nav-edit-properties">
<div class="nav-info-column-filename"></div>
@ -107,13 +122,44 @@
</div>
<div class="vertical-spacer"></div>
<div class="nav-btn-group">
<button class="pf-c-button pf-m-danger" id="nav-cancel-edit-btn">Cancel</button>
<button class="pf-c-button pf-m-danger" id="nav-cancel-edit-btn"><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">Apply</button>
<button class="pf-c-button pf-m-primary" id="nav-apply-edit-btn"><i class="fas fa-save"></i></button>
</div>
</div>
</div>
</div>
<div class="flex-row nav-footer">
<div>
<span id="nav-num-dirs">-</span> Directories, <span id="nav-num-files">-</span> Files
</div>
<div class="spacer-stretchy"></div>
<a href="https://45drives.com" target="_blank">
<img src="branding/logo-light.svg" id="logo-45d"><span class="logo-45">45</span><span class="logo-drives">Drives</span>
</a>
<div class="spacer-stretchy"></div>
<div class="nav-toggle">
<div class="nav-btn-group">
<i class="fas fa-low-vision" id="nav-show-hidden-icon"></i>
<div class="horizontal-spacer-sm"></div>
<label class="switch">
<input type="checkbox" id="nav-show-hidden">
<span class="slider round"></span>
</label>
</div>
</div>
<div class="horizontal-spacer"></div>
<div class="nav-toggle">
<div class="nav-btn-group">
<i class="fas fa-sun" id="houston-theme-icon"></i>
<div class="horizontal-spacer-sm"></div>
<label class="switch">
<input type="checkbox" id="toggle-theme">
<span class="slider round"></span>
</label>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,13 +1,32 @@
function property_entry_html(key, value) {
/*
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
Copyright (C) 2021 Sam Silver <ssilver@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/>.
*/
function property_entry_html(/*string*/ key, /*string*/ 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>";
html += '<span class="nav-property-pair-key">' + key + '</span>';
html += '<span class="nav-property-pair-value">' + value + '</span>';
html += '</div>';
return html;
}
function format_bytes(bytes) {
if (bytes === 0) return "0 B";
function format_bytes(/*int*/ 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);
@ -15,7 +34,7 @@ function format_bytes(bytes) {
return formatted.toFixed(2).toString() + units[index];
}
function format_time(timestamp) {
function format_time(/*int*/ timestamp) {
var date = new Date(timestamp * 1000);
return date.toLocaleString();
}
@ -35,72 +54,71 @@ function format_permissions(/*int*/ mode) {
return result;
}
/*
* Code to change theme
*/
const toggleSwitch = document.getElementById("toggle-theme");
function switchTheme(e) {
if (e.target.checked) {
document.documentElement.setAttribute("data-theme", "dark");
} else {
function set_last_theme_state() {
var toggle_switch = document.getElementById("toggle-theme");
var state = localStorage.getItem("houston-theme-state");
var icon = document.getElementById("houston-theme-icon");
var logo = document.getElementById("logo-45d");
if (state === "light") {
toggle_switch.checked = false;
document.documentElement.setAttribute("data-theme", "light");
}
}
toggleSwitch.addEventListener("change", switchTheme, false);
/* cephfs_dir_stats
* Receives: path to folder
* Does: Tries command with --json flag at path to folder. If
* command fails and gives an error then catch that error
* Returns: JSON object or nothing
*/
async function cephfs_dir_stats(path) {
try {
var proc = await cockpit.spawn(["cephfs-dir-stats", "-j", path], {
err: "ignore",
});
return JSON.parse(proc.replace(/\[|\]/g, ""));
} catch {
return [];
}
}
/* in_json
* Receives: A boolean to see if key is in JSON object
* and the JSON objects value if it exists
* Does: Checks if JSON key exists; if so, return the
* value of the key, if not, return the string "N/A"
* Returns: The value of key or "N/A"
*/
function in_json(is_key, value) {
if (is_key) {
return value;
icon.classList.remove("fa-moon");
icon.classList.add("fa-sun");
logo.src = "branding/logo-light.svg";
} else if (state === "dark") {
toggle_switch.checked = true;
document.documentElement.setAttribute("data-theme", "dark");
icon.classList.remove("fa-sun");
icon.classList.add("fa-moon");
logo.src = "branding/logo-dark.svg";
} else {
return "N/A";
toggle_switch.checked = false;
state = "light";
localStorage.setItem("houston-theme-state", state);
logo.src = "branding/logo-light.svg";
}
}
function switch_theme(/*event*/ e) {
var icon = document.getElementById("houston-theme-icon");
var logo = document.getElementById("logo-45d");
var state = "";
if (e.target.checked) {
state = "dark";
icon.classList.remove("fa-sun");
icon.classList.add("fa-moon");
logo.src = "branding/logo-dark.svg";
} else {
state = "light";
icon.classList.remove("fa-moon");
icon.classList.add("fa-sun");
logo.src = "branding/logo-light.svg";
}
document.documentElement.setAttribute("data-theme", state);
localStorage.setItem("houston-theme-state", state);
}
class NavEntry {
constructor(/*string or array*/ path, /*dict*/ stat, /*NavWindow*/ nav_window_ref) {
this.nav_window_ref = nav_window_ref;
if (typeof path == "string") this.path = path.split("/").splice(1);
else this.path = path;
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"));
let icon = this.dom_element.nav_item_icon = document.createElement("i");
icon.classList.add("nav-file-icon");
let title = (this.dom_element.nav_item_title = document.createElement("div"));
let title = this.dom_element.nav_item_title = document.createElement("div");
title.classList.add("nav-item-title");
title.innerText = this.filename();
this.dom_element.appendChild(icon);
this.dom_element.appendChild(title);
this.stat = stat;
this.dom_element.addEventListener("click", this);
this.is_hidden_file = this.filename().startsWith('.');
}
handleEvent(e) {
handleEvent(/*event*/ e) {
switch (e.type) {
case "click":
this.show_properties();
@ -113,16 +131,21 @@ class NavEntry {
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);
if (this.dom_element.parentElement)
this.dom_element.parentElement.removeChild(this.dom_element);
}
filename() {
var name = this.path[this.path.length - 1];
if (name === "") name = "/";
if (name === "")
name = "/";
return name;
}
path_str() {
return "/" + this.path.join("/");
}
parent_dir() {
return this.path.slice(0, this.path.length - 1);
}
show() {
document.getElementById("nav-contents-view").appendChild(this.dom_element);
}
@ -133,20 +156,20 @@ class NavEntry {
return this.stat["mode"] & 0o777;
}
async chmod(/*int*/ new_perms) {
var proc = cockpit.spawn(["chmod", (new_perms & 0o777).toString(8), this.path_str()], {
superuser: "try",
err: "out",
});
var proc = cockpit.spawn(
["chmod", (new_perms & 0o777).toString(8), this.path_str()],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
await proc;
}
async chown(/*string*/ new_owner, /*string*/ new_group) {
var proc = cockpit.spawn(["chown", [new_owner, new_group].join(":"), this.path_str()], {
superuser: "try",
err: "out",
});
var proc = cockpit.spawn(
["chown", [new_owner, new_group].join(":"), this.path_str()],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
@ -181,20 +204,14 @@ class NavEntry {
populate_edit_fields() {
document.getElementById("nav-edit-filename").value = this.filename();
var mode_bits = [
"other-exec",
"other-write",
"other-read",
"group-exec",
"group-write",
"group-read",
"owner-exec",
"owner-write",
"owner-read",
"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(mode_bits[i]).checked = (result != 0);
}
document.getElementById("nav-edit-owner").value = this.stat["owner"];
document.getElementById("nav-edit-group").value = this.stat["group"];
@ -206,36 +223,17 @@ class NavFile extends NavEntry {
super(path, stat, nav_window_ref);
this.nav_type = "file";
this.dom_element.nav_item_icon.classList.add("fas", "fa-file");
}
handleEvent(e) {
super.handleEvent(e);
}
async rm() {
var proc = cockpit.spawn(["rm", "-f", this.path_str()], { superuser: "try", err: "out" });
proc.fail((e, data) => {
window.alert(data);
});
await proc;
}
}
class NavDir extends NavEntry {
constructor(/*string or array*/ path, /*dict*/ stat, nav_window_ref) {
super(path, stat, nav_window_ref); // super = call parent
this.nav_type = "dir";
this.dom_element.nav_item_icon.classList.add("fas", "fa-folder");
this.double_click = false;
this.ceph_stats = [];
cephfs_dir_stats(this.path_str()).then((data) => (this.ceph_stats = data));
}
handleEvent(e) {
handleEvent(/*event*/ e) {
switch(e.type){
case "click":
if (this.double_click) this.nav_window_ref.cd(this);
else {
// single click
if(this.double_click)
this.show_edit_file_contents();
else{ // single click
this.double_click = true;
if (this.timeout) clearTimeout(this.timeout);
if(this.timeout)
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.double_click = false;
}, 500);
@ -244,75 +242,171 @@ class NavDir extends NavEntry {
}
super.handleEvent(e);
}
async get_children(nav_window_ref) {
var children = [];
var data = await cockpit.spawn(["/usr/share/cockpit/navigator/scripts/ls.py", this.path_str()], {
err: "ignore",
});
var response = JSON.parse(data);
this.stat = response["."]["stat"];
var entries = response["children"];
entries.forEach((entry) => {
var filename = entry["filename"];
var path = this.path.length >= 1 && this.path[0] ? [...this.path, filename] : [filename];
var stat = entry["stat"];
if (entry["isdir"]) children.push(new NavDir(path, stat, nav_window_ref));
else children.push(new NavFile(path, stat, nav_window_ref));
});
children.sort((first, second) => {
if (first.nav_type === second.nav_type) {
return first.filename().localeCompare(second.filename());
}
if (first.nav_type === "dir") return -1;
return 1;
});
return children;
}
async rm() {
var proc = cockpit.spawn(["rmdir", this.path_str()], { superuser: "try", err: "out" });
var proc = cockpit.spawn(
["rm", "-f", this.path_str()],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
await proc;
}
show_properties() {
var extra_properties = "";
async show_edit_file_contents() {
for (let button of document.getElementsByTagName("button")) {
if (!button.classList.contains("editor-btn"))
button.disabled = true;
}
document.getElementById("pwd").disabled = true;
var proc_output = await cockpit.spawn(["file", "--mime-type", this.path_str()], {superuser: "try"});
var fields = proc_output.split(':');
var type = fields[1].trim();
if(!(type.match(/^text/) || type.match(/^inode\/x-empty$/) || this.stat["size"] === 0)){
if(!window.confirm("File is of type `" + type + "`. Are you sure you want to edit it?"))
return;
}
var contents = await cockpit.spawn(["cat", this.path_str()], {superuser: "try"});
document.getElementById("nav-edit-contents-textarea").value = contents;
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;
await cockpit.script("echo -n \"$1\" > $2", [new_contents, this.path_str()], {superuser: "try"});
this.nav_window_ref.refresh();
this.hide_edit_file_contents();
}
hide_edit_file_contents() {
document.getElementById("nav-edit-contents-view").style.display = "none";
document.getElementById("nav-contents-view").style.display = "flex";
for (let button of document.getElementsByTagName("button")) {
button.disabled = false;
}
document.getElementById("pwd").disabled = false;
}
}
class NavDir extends NavEntry {
constructor(/*string or array*/ path, /*dict*/ 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;
}
handleEvent(/*event*/ 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);
}
async get_children(/*NavWindow*/ nav_window_ref, /*boolean*/ no_alert = false) {
var children = [];
var proc = cockpit.spawn(
["/usr/share/cockpit/navigator/scripts/ls.py", this.path_str()],
{err:"out", superuser: "try"}
);
proc.fail((e, data) => {
if(!no_alert)
window.alert(data);
});
var data = await proc;
var response = JSON.parse(data);
this.stat = response["."]["stat"];
var entries = response["children"];
entries.forEach((entry) => {
var filename = entry["filename"];
var path = (this.path.length >= 1 && this.path[0]) ? [...this.path, filename] : [filename];
var stat = entry["stat"];
if (entry["isdir"])
children.push(new NavDir(path, stat, nav_window_ref));
else
children.push(new NavFile(path, stat, nav_window_ref));
});
children.sort((first, second) => {
if (first.nav_type === second.nav_type) {
return first.filename().localeCompare(second.filename());
}
if (first.nav_type === "dir")
return -1;
return 1;
});
return children;
}
async rm() {
var proc = cockpit.spawn(
["rmdir", this.path_str()],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
await proc;
}
async cephfs_dir_stats() {
try {
var proc = await cockpit.spawn(
["cephfs-dir-stats", "-j", this.path_str()],
{err: "ignore"}
);
return JSON.parse(proc)[0];
} catch {
return null;
}
}
async show_properties() {
var 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.length !== 0) {
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",
in_json(this.ceph_stats.hasOwnProperty("files"), this.ceph_stats.files)
this.ceph_stats.hasOwnProperty("files") ? this.ceph_stats.files : "N/A"
);
extra_properties += property_entry_html(
"Directories",
in_json(this.ceph_stats.hasOwnProperty("subdirs"), this.ceph_stats.subdirs)
this.ceph_stats.hasOwnProperty("subdirs") ? this.ceph_stats.subdirs : "N/A"
);
extra_properties += property_entry_html(
"Recursive files",
in_json(this.ceph_stats.hasOwnProperty("rfiles"), this.ceph_stats.rfiles)
this.ceph_stats.hasOwnProperty("rfiles") ? this.ceph_stats.rfiles : "N/A"
);
extra_properties += property_entry_html(
"Recursive directories",
in_json(this.ceph_stats.hasOwnProperty("rsubdirs"), this.ceph_stats.rsubdirs)
this.ceph_stats.hasOwnProperty("rsubdirs") ? this.ceph_stats.rsubdirs : "N/A"
);
extra_properties += property_entry_html(
"Total size",
in_json(this.ceph_stats.hasOwnProperty("rbytes"), this.ceph_stats.rbytes)
this.ceph_stats.hasOwnProperty("rbytes") ? this.ceph_stats.rbytes : "N/A"
);
extra_properties += property_entry_html(
"Layout pool",
in_json(this.ceph_stats.hasOwnProperty("layout.pool"), this.ceph_stats["layout.pool"])
this.ceph_stats.hasOwnProperty("layout.pool") ? this.ceph_stats["layout.pool"] : "N/A"
);
extra_properties += property_entry_html(
"Max files",
in_json(this.ceph_stats.hasOwnProperty("max_files"), this.ceph_stats.max_files)
this.ceph_stats.hasOwnProperty("max_files") ? this.ceph_stats.max_files : "N/A"
);
extra_properties += property_entry_html(
"Max bytes",
in_json(this.ceph_stats.hasOwnProperty("max_bytes"), this.ceph_stats.max_bytes)
this.ceph_stats.hasOwnProperty("max_bytes") ? this.ceph_stats.max_bytes : "N/A"
);
}
super.show_properties(extra_properties);
@ -322,6 +416,7 @@ class NavDir extends NavEntry {
class NavWindow {
constructor() {
this.path_stack = [new NavDir("/", this)];
this.path_stack_index = this.path_stack.length - 1;
this.selected_entry = this.pwd();
this.entries = [];
this.window = document.getElementById("nav-contents-view");
@ -336,34 +431,54 @@ class NavWindow {
}
}
async refresh() {
var num_dirs = 0;
var num_files = 0;
var show_hidden = document.getElementById("nav-show-hidden").checked;
this.start_load();
var files = await this.pwd().get_children(this);
while (this.entries.length) {
var entry = this.entries.pop();
entry.destroy();
}
files.forEach((file) => {
if (file.nav_type === "dir")
num_dirs++;
else
num_files++;
if(!file.is_hidden_file || show_hidden)
file.show();
this.entries.push(file);
});
document.getElementById("pwd").innerText = this.pwd().path_str();
document.getElementById("pwd").value = this.pwd().path_str();
this.set_selected(this.pwd());
this.show_selected_properties();
document.getElementById("nav-num-dirs").innerText = num_dirs.toString();
document.getElementById("nav-num-files").innerText = num_files.toString();
this.stop_load();
}
pwd() {
return this.path_stack[this.path_stack.length - 1];
return this.path_stack[this.path_stack_index];
}
cd(new_dir) {
cd(/*NavDir*/ 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().catch(() => {
this.path_stack.pop();
this.refresh();
window.alert(new_dir.path_str() + " is inaccessible.");
this.back();
});
}
up() {
if (this.path_stack.length > 1) this.path_stack.pop();
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()));
}
show_selected_properties() {
this.selected_entry.show_properties();
}
@ -424,7 +539,8 @@ class NavWindow {
var bit = 0;
for (let category of category_list) {
for (let action of action_list) {
if (document.getElementById(category + "-" + action).checked) result |= 1 << bit;
if (document.getElementById(category + "-" + action).checked)
result |= 1 << bit;
bit++;
}
}
@ -470,15 +586,16 @@ class NavWindow {
}
async mkdir() {
var new_dir_name = window.prompt("Directory Name: ");
if (new_dir_name === null) return;
if (new_dir_name === null)
return;
if (new_dir_name.includes("/")) {
window.alert("Directory name can't contain `/`.");
return;
}
var proc = cockpit.spawn(["mkdir", this.pwd().path_str() + "/" + new_dir_name], {
superuser: "try",
err: "out",
});
var proc = cockpit.spawn(
["mkdir", this.pwd().path_str() + "/" + new_dir_name],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
@ -487,49 +604,113 @@ class NavWindow {
}
async touch() {
var new_file_name = window.prompt("File Name: ");
if (new_file_name === null) return;
if (new_file_name === null)
return;
if (new_file_name.includes("/")) {
window.alert("File name can't contain `/`.");
return;
}
var proc = cockpit.spawn(["touch", this.pwd().path_str() + "/" + new_file_name], {
superuser: "try",
err: "out",
});
var proc = cockpit.spawn(
["touch", this.pwd().path_str() + "/" + new_file_name],
{superuser: "try", err: "out"}
);
proc.fail((e, data) => {
window.alert(data);
});
await proc;
this.refresh();
}
nav_bar_event_handler(/*event*/ e) {
switch(e.key){
case 'Enter':
this.nav_bar_cd();
break;
default:
break;
}
}
nav_bar_cd() {
var new_path = document.getElementById("pwd").value;
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);
console.log(parent_dir.path_str());
var error = false;
var objs = await parent_dir.get_children(this, true).catch(() => {error = true});
if(error)
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").hidden = false;
var buttons = document.getElementsByTagName("button");
for (let button of buttons) {
button.disabled = true;
}
}
stop_load() {
document.getElementById("nav-loader-container").hidden = true;
var buttons = document.getElementsByTagName("button");
for (let button of buttons) {
button.disabled = false;
}
}
toggle_show_hidden(e) {
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();
}
}
let nav_window = new NavWindow();
function set_up_buttons() {
document.getElementById("nav-back-btn").addEventListener("click", nav_window.back.bind(nav_window));
document.getElementById("nav-forward-btn").addEventListener("click", nav_window.forward.bind(nav_window));
document.getElementById("nav-up-dir-btn").addEventListener("click", nav_window.up.bind(nav_window));
document.getElementById("nav-refresh-btn").addEventListener("click", nav_window.refresh.bind(nav_window));
document.getElementById("nav-mkdir-btn").addEventListener("click", nav_window.mkdir.bind(nav_window));
document.getElementById("nav-touch-btn").addEventListener("click", nav_window.touch.bind(nav_window));
document
.getElementById("nav-delete-btn")
.addEventListener("click", nav_window.delete_selected.bind(nav_window));
document
.getElementById("nav-edit-properties-btn")
.addEventListener("click", nav_window.show_edit_selected.bind(nav_window));
document
.getElementById("nav-cancel-edit-btn")
.addEventListener("click", nav_window.hide_edit_selected.bind(nav_window));
document
.getElementById("nav-apply-edit-btn")
.addEventListener("click", nav_window.apply_edit_selected.bind(nav_window));
document.getElementById("nav-delete-btn").addEventListener("click", nav_window.delete_selected.bind(nav_window));
document.getElementById("nav-edit-properties-btn").addEventListener("click", nav_window.show_edit_selected.bind(nav_window));
document.getElementById("nav-cancel-edit-btn").addEventListener("click", nav_window.hide_edit_selected.bind(nav_window));
document.getElementById("nav-apply-edit-btn").addEventListener("click", nav_window.apply_edit_selected.bind(nav_window));
var mode_checkboxes = document.getElementsByClassName("mode-checkbox");
for (let checkbox of mode_checkboxes) {
checkbox.addEventListener("change", nav_window.update_permissions_preview.bind(nav_window));
}
document.getElementById("pwd").addEventListener("input", nav_window.nav_bar_update_choices.bind(nav_window), false);
document.getElementById("pwd").addEventListener("focus", nav_window.nav_bar_update_choices.bind(nav_window), false);
document.getElementById("pwd").addEventListener("keydown", nav_window.nav_bar_event_handler.bind(nav_window));
document.getElementById("toggle-theme").addEventListener("change", switch_theme, false);
document.getElementById("nav-show-hidden").addEventListener("change", nav_window.toggle_show_hidden.bind(nav_window));
}
async function main() {
set_last_theme_state();
await nav_window.refresh();
set_up_buttons();
}

View File

@ -0,0 +1,139 @@
#!/usr/bin/env python3
import sys
import re
import subprocess
import math
import json
from optparse import OptionParser
###############################################################################
# Name: dir_attributes
# Args: path to directory, command to run
# Desc: executes getfattr to retrive ceph dir attribute and returns as dictionary
###############################################################################
def dir_attributes(path, type, command):
attrs = {}
try:
child = subprocess.Popen(
["getfattr", "-n", "ceph." + type + "." + command, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
output_string, err = child.communicate()
except OSError:
print("Error executing getfattr. Is xattr installed?")
sys.exit(1)
if child.wait() != 0:
if err.find("No such attribute") != -1:
return {}
else:
print("Error executing getfattr. Is xattr installed?")
sys.exit(1)
# return {}
fields = re.findall(
r"^ceph\.[dir|quota]+\.([^=]+)=\"([^\"]+)\"$", output_string, re.MULTILINE)
if len(fields) == 0:
print(f'No ceph xattrs, is {path} in a ceph filesystem?')
sys.exit(1)
attrs[fields[0][0]] = fields[0][1]
return attrs
###############################################################################
# Name: run_dir_commands
# Args: path to directory
# Desc: calls dir_attributes and quota_attrivutes multiple times each with a different command
# Retu: Returns the outputs of all commands in a dictionary
###############################################################################
def run_dir_commands(path):
outputs = {'path':path}
dirList = ["entries", "files", "rbytes", "rentries",
"rfiles", "rsubdirs", "subdirs", "layout.pool"]
quotaList = ["max_files", "max_bytes"]
for items in dirList:
outputs.update(dir_attributes(path, "dir", items))
for items in quotaList:
outputs.update(dir_attributes(path, "quota", items))
if "rbytes" in outputs.keys():
outputs["rbytes"] = format_bytes(int(outputs["rbytes"]))
if "max_bytes" in outputs.keys():
outputs["max_bytes"] = format_bytes(int(outputs["max_bytes"]))
return outputs
###############################################################################
# Name: display_attributes
# Args: path to directory
# Desc: calls run_dir_commands and prints output
###############################################################################
def display_attributes(path):
attrs = run_dir_commands(path)
print(path, ":")
max_width = len(
max(attrs.values(), key=lambda x: len(x.split(" ")[0])).split(" ")[0])
print("Files: ", "{0:>{1}}".format(
attrs["files"], max_width) if "files" in attrs.keys() else "N/A")
print("Directories: ", "{0:>{1}}".format(
attrs["subdirs"], max_width) if "subdirs" in attrs.keys() else "N/A")
print("Recursive Files: ", "{0:>{1}}".format(
attrs["rfiles"], max_width) if "rfiles" in attrs.keys() else "N/A")
print("Recursive Directories: ", "{0:>{1}}".format(
attrs["rsubdirs"], max_width) if "rsubdirs" in attrs.keys() else "N/A")
print("Total Size: ", "{0:>{1}}".format(attrs["rbytes"].split(" ")[
0], max_width) + " " + attrs["rbytes"].split(" ")[1] if "rbytes" in attrs.keys() else "N/A")
print("Layout Pool: ", "{0:>{1}}".format(
attrs["layout.pool"], max_width) if "layout.pool" in attrs.keys() else "N/A")
print("Max Files: ", "{0:>{1}}".format(
attrs["max_files"], max_width) if "max_files" in attrs.keys() else " N/A")
print("Max Bytes: ", "{0:>{1}}".format(
attrs["max_bytes"], max_width) if "max_bytes" in attrs.keys() else " N/A")
print()
################################################################################
# Name: format_bytes
# Args: integer value in bytes
# Desc: formats size_bytes in SI base units and returns as string
################################################################################
def format_bytes(size_bytes):
if size_bytes == 0:
return "0 B"
size_name = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])
###############################################################################
# Name: main (cephfs-dir-stats)
# Args: (see parser)
# Desc: lists recursive ceph stats of specified directory
###############################################################################
def main():
parser = OptionParser()
parser.add_option("-j", "--json", help="output stats in JSON format.", action="store_true", dest="json", default=False)
(options, args) = parser.parse_args()
if len(args) == 0:
args = ["."]
if(options.json):
obj = []
for arg in args:
obj.append(run_dir_commands(arg))
obj = json.dumps(obj)
print(obj)
else:
for arg in args:
display_attributes(arg)
if __name__ == "__main__":
main()

View File

@ -1,5 +1,22 @@
#!/usr/bin/env python3
"""
Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>.
"""
import os
from stat import S_ISDIR, filemode
import json
@ -14,6 +31,16 @@ def get_stat(full_path, filename = '/'):
isdir = S_ISDIR(os.stat(full_path).st_mode)
except OSError:
pass
owner = '?'
try:
owner = getpwuid(stats.st_uid).pw_name
except:
pass
group = '?'
try:
group = getgrgid(stats.st_gid).gr_name
except:
pass
response = {
"filename": filename,
"isdir": isdir,
@ -21,9 +48,9 @@ def get_stat(full_path, filename = '/'):
"mode": stats.st_mode,
"mode-str": filemode(stats.st_mode),
"uid": stats.st_uid,
"owner": getpwuid(stats.st_uid).pw_name,
"owner": owner,
"gid": stats.st_gid,
"group": getgrgid(stats.st_gid).gr_name,
"group": group,
"size": stats.st_size,
"atime": stats.st_atime,
"mtime": stats.st_mtime,