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.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Samba Manager.
Cockpit Samba Manager 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,
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/>.
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/>.
-->
<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);
});
@ -155,7 +178,7 @@ class NavEntry {
async mv(/*string*/ new_path) {
var proc = cockpit.spawn(
["mv", "-n", this.path_str(), [this.nav_window_ref.pwd().path_str(), new_path].join("/")],
{ superuser: "try", err: "out" }
{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) {
switch (e.type) {
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) => {
file.show();
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();
}
@ -401,8 +516,8 @@ class NavWindow {
if (
!window.confirm(
"Warning: editing `" +
this.selected_entry.path_str() +
"` can be dangerous. Are you sure?"
this.selected_entry.path_str() +
"` can be dangerous. Are you sure?"
)
) {
return;
@ -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,