mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-30 17:15:16 +02:00
commit
da70193ef4
17
README.md
17
README.md
@ -7,7 +7,10 @@ With no command line use needed, you can:
|
|||||||
* Create, delete, and rename files,
|
* Create, delete, and rename files,
|
||||||
* Edit file contents,
|
* Edit file contents,
|
||||||
* Edit file ownership and permissions,
|
* Edit file ownership and permissions,
|
||||||
* Create symbolic links to files and directories.
|
* Create symbolic links to files and directories,
|
||||||
|
* Reorganize files through cut, copy, and paste,
|
||||||
|
* **Upload files by dragging and dropping**,
|
||||||
|
* **Download files and directories**.
|
||||||
|
|
||||||
### Browsing Filesystem
|
### Browsing Filesystem
|
||||||

|

|
||||||
@ -19,17 +22,17 @@ With no command line use needed, you can:
|
|||||||
# Installation
|
# Installation
|
||||||
## From Github Release
|
## From Github Release
|
||||||
### Ubuntu
|
### Ubuntu
|
||||||
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator_0.3.0-1focal_all.deb`
|
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.4.0/cockpit-navigator_0.4.0-1focal_all.deb`
|
||||||
1. `# apt install ./cockpit-navigator_0.3.0-1focal_all.deb`
|
1. `# apt install ./cockpit-navigator_0.4.0-1focal_all.deb`
|
||||||
### EL7
|
### EL7
|
||||||
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator-0.3.0-1.el7.noarch.rpm`
|
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.4.0/cockpit-navigator-0.4.0-1.el7.noarch.rpm`
|
||||||
### EL8
|
### EL8
|
||||||
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.3.0/cockpit-navigator-0.3.0-1.el8.noarch.rpm`
|
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.4.0/cockpit-navigator-0.4.0-1.el8.noarch.rpm`
|
||||||
## From Source
|
## From Source
|
||||||
1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`.
|
1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`, `zip`.
|
||||||
1. `$ git clone https://github.com/45Drives/cockpit-navigator.git`
|
1. `$ git clone https://github.com/45Drives/cockpit-navigator.git`
|
||||||
1. `$ cd cockpit-navigator`
|
1. `$ cd cockpit-navigator`
|
||||||
1. `$ git checkout <version>` (v0.3.0 is latest)
|
1. `$ git checkout <version>` (v0.4.0 is latest)
|
||||||
1. `# make install`
|
1. `# make install`
|
||||||
## From 45Drives Repositories
|
## From 45Drives Repositories
|
||||||
### Ubuntu
|
### Ubuntu
|
||||||
|
8
debian/changelog
vendored
8
debian/changelog
vendored
@ -1,3 +1,11 @@
|
|||||||
|
cockpit-navigator (0.4.0-1focal) focal; urgency=low
|
||||||
|
|
||||||
|
* Add icons to right click menu.
|
||||||
|
* Add ability to download files and directories.
|
||||||
|
* Show transfer rate and ETA while uploading files.
|
||||||
|
|
||||||
|
-- Josh Boudreau <jboudreau@45drives.com> Mon, 07 Jun 2021 12:09:00 -0300
|
||||||
|
|
||||||
cockpit-navigator (0.3.0-1focal) focal; urgency=medium
|
cockpit-navigator (0.3.0-1focal) focal; urgency=medium
|
||||||
|
|
||||||
* Add drag and drop uploading of files.
|
* Add drag and drop uploading of files.
|
||||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -9,5 +9,5 @@ Homepage: https://github.com/45Drives/cockpit-navigator
|
|||||||
Package: cockpit-navigator
|
Package: cockpit-navigator
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends},
|
Depends: ${shlibs:Depends}, ${misc:Depends},
|
||||||
cockpit, python3, rsync
|
cockpit, python3, rsync, zip
|
||||||
Description: A File System Browser for Cockpit.
|
Description: A File System Browser for Cockpit.
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
Name: cockpit-navigator
|
Name: cockpit-navigator
|
||||||
Version: 0.3.0
|
Version: 0.4.0
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: A File System Browser for Cockpit.
|
Summary: A File System Browser for Cockpit.
|
||||||
License: GPL-3.0+
|
License: GPL-3.0+
|
||||||
URL: github.com/45drives/cockpit-navigator/blob/main/README.md
|
URL: github.com/45drives/cockpit-navigator/blob/main/README.md
|
||||||
Source0: %{name}-%{version}.tar.gz
|
Source0: %{name}-%{version}.tar.gz
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
Requires: cockpit python3 rsync
|
Requires: cockpit python3 rsync zip
|
||||||
|
|
||||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
|
||||||
|
|
||||||
@ -32,6 +32,10 @@ rm -rf %{buildroot}
|
|||||||
/usr/share/cockpit/navigator/*
|
/usr/share/cockpit/navigator/*
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Jun 07 2021 Josh Boudreau <jboudreau@45drives.com> 0.4.0-1
|
||||||
|
- Add icons to right click menu.
|
||||||
|
- Add ability to download files and directories.
|
||||||
|
- Show transfer rate and ETA while uploading files.
|
||||||
* Thu Jun 03 2021 Josh Boudreau <jboudreau@45drives.com> 0.3.0-1
|
* Thu Jun 03 2021 Josh Boudreau <jboudreau@45drives.com> 0.3.0-1
|
||||||
- Add drag and drop uploading of files.
|
- Add drag and drop uploading of files.
|
||||||
- Add event listeners for ctrl+a to select all, ctrl+x to cut,
|
- Add event listeners for ctrl+a to select all, ctrl+x to cut,
|
||||||
|
@ -480,14 +480,24 @@ input:checked + .slider:before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-context-menu-item {
|
.nav-context-menu-item {
|
||||||
padding: 0 12px 0 12px;
|
padding: 0 12px 0 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-context-menu-item:hover {
|
.nav-context-menu-item:hover {
|
||||||
background-color: var(--border);
|
background-color: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-context-menu-item > div {
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.drag-enter {
|
.drag-enter {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--border);
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,27 @@ function format_time(timestamp) {
|
|||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} seconds
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
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
|
* @param {number} mode
|
||||||
@ -138,6 +159,44 @@ function switch_theme(e) {
|
|||||||
localStorage.setItem("houston-theme-state", state);
|
localStorage.setItem("houston-theme-state", state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class NavEntry {
|
class NavEntry {
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -246,26 +305,35 @@ class NavEntry {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {number} new_perms
|
* @param {number} new_perms
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async chmod(new_perms) {
|
chmod(new_perms) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["chmod", (new_perms & 0o777).toString(8), this.path_str()],
|
["chmod", (new_perms & 0o777).toString(8), this.path_str()],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
|
});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await proc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} new_owner
|
* @param {string} new_owner
|
||||||
* @param {string} new_group
|
* @param {string} new_group
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async chown(new_owner, new_group) {
|
chown(new_owner, new_group) {
|
||||||
if (!new_owner && !new_group)
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!new_owner && !new_group) {
|
||||||
|
resolve();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
var cmd = "";
|
var cmd = "";
|
||||||
var arg = "";
|
var arg = "";
|
||||||
if (new_group && !new_owner) {
|
if (new_group && !new_owner) {
|
||||||
@ -281,25 +349,33 @@ class NavEntry {
|
|||||||
[cmd, arg, this.path_str()],
|
[cmd, arg, this.path_str()],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
|
});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await proc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} new_path
|
* @param {string} new_path
|
||||||
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async mv(new_path) {
|
mv(new_path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["mv", "-n", this.path_str(), [this.nav_window_ref.pwd().path_str(), new_path].join("/")],
|
["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) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
|
});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await proc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -363,7 +439,7 @@ class NavFile extends NavEntry {
|
|||||||
switch(e.type){
|
switch(e.type){
|
||||||
case "click":
|
case "click":
|
||||||
if (this.double_click)
|
if (this.double_click)
|
||||||
this.show_edit_file_contents();
|
this.open();
|
||||||
else { // single click
|
else { // single click
|
||||||
this.double_click = true;
|
this.double_click = true;
|
||||||
if(this.timeout)
|
if(this.timeout)
|
||||||
@ -384,28 +460,43 @@ class NavFile extends NavEntry {
|
|||||||
super.handleEvent(e);
|
super.handleEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
async rm() {
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
rm() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["rm", "-f", this.path_str()],
|
["rm", "-f", this.path_str()],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
});
|
});
|
||||||
await proc;
|
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 (window.confirm("Can't open " + this.filename() + " for editing. Download?")) {
|
||||||
|
var download = new NavDownloader(this);
|
||||||
|
download.download();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async show_edit_file_contents() {
|
async show_edit_file_contents() {
|
||||||
this.nav_window_ref.disable_buttons_for_editing();
|
this.nav_window_ref.disable_buttons_for_editing();
|
||||||
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?")) {
|
|
||||||
this.nav_window_ref.enable_buttons();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var contents = "";
|
var contents = "";
|
||||||
try {
|
try {
|
||||||
contents = await cockpit.file(this.path_str(), {superuser: "try"}).read();
|
contents = await cockpit.file(this.path_str(), {superuser: "try"}).read();
|
||||||
@ -488,7 +579,7 @@ class NavFileLink extends NavFile{
|
|||||||
var proc_output = await cockpit.spawn(["file", "--mime-type", target_path], {superuser: "try"});
|
var proc_output = await cockpit.spawn(["file", "--mime-type", target_path], {superuser: "try"});
|
||||||
var fields = proc_output.split(':');
|
var fields = proc_output.split(':');
|
||||||
var type = fields[1].trim();
|
var type = fields[1].trim();
|
||||||
if (!(type.match(/^text/) || type.match(/^inode\/x-empty$/) || this.stat["size"] === 0)) {
|
if (!(/^text/.test(type) || /^inode\/x-empty$/.test(type) || this.stat["size"] === 0)) {
|
||||||
if (!window.confirm("File is of type `" + type + "`. Are you sure you want to edit it?")) {
|
if (!window.confirm("File is of type `" + type + "`. Are you sure you want to edit it?")) {
|
||||||
this.nav_window_ref.enable_buttons();
|
this.nav_window_ref.enable_buttons();
|
||||||
return;
|
return;
|
||||||
@ -565,27 +656,33 @@ class NavDir extends NavEntry {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {NavWindow} nav_window_ref
|
* @param {NavWindow} nav_window_ref
|
||||||
* @param {boolean} no_alert
|
* @returns {Promise<NavEntry[]>}
|
||||||
* @returns {object[]}
|
|
||||||
*/
|
*/
|
||||||
async get_children(nav_window_ref, no_alert = false) {
|
get_children(nav_window_ref) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
var children = [];
|
var children = [];
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["/usr/share/cockpit/navigator/scripts/ls.py", this.path_str()],
|
["/usr/share/cockpit/navigator/scripts/ls.py", this.path_str()],
|
||||||
{err:"out", superuser: "try"}
|
{err:"out", superuser: "try"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.fail((e, data) => {
|
||||||
if(!no_alert)
|
reject(data);
|
||||||
window.alert(data);
|
|
||||||
});
|
});
|
||||||
var data = await proc;
|
var data;
|
||||||
|
try {
|
||||||
|
data = await proc;
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var response = JSON.parse(data);
|
var response = JSON.parse(data);
|
||||||
this.stat = response["."]["stat"];
|
this.stat = response["."]["stat"];
|
||||||
var entries = response["children"];
|
var entries = response["children"];
|
||||||
|
var filename, path, stat;
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
var filename = entry["filename"];
|
filename = entry["filename"];
|
||||||
var path = (this.path.length >= 1 && this.path[0]) ? [...this.path, filename] : [filename];
|
path = (this.path.length >= 1 && this.path[0]) ? [...this.path, filename] : [filename];
|
||||||
var stat = entry["stat"];
|
stat = entry["stat"];
|
||||||
switch(stat["mode-str"].charAt(0)) {
|
switch(stat["mode-str"].charAt(0)) {
|
||||||
case 'd':
|
case 'd':
|
||||||
children.push(new NavDir(path, stat, nav_window_ref));
|
children.push(new NavDir(path, stat, nav_window_ref));
|
||||||
@ -601,18 +698,51 @@ class NavDir extends NavEntry {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return children;
|
resolve(children);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async rm() {
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
rm() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["rmdir", this.path_str()],
|
["rmdir", this.path_str()],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
|
});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
if (/^rmdir: failed to remove .*: Directory not empty\n?$/.test(data)) {
|
||||||
|
if (window.confirm("WARNING: '" + this.path_str() + "' is not empty. Delete recursively? This can NOT be undone.")) {
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
await proc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -697,15 +827,23 @@ class NavDirLink extends NavDir{
|
|||||||
this.dom_element.nav_item_title.style.fontStyle = "italic";
|
this.dom_element.nav_item_title.style.fontStyle = "italic";
|
||||||
}
|
}
|
||||||
|
|
||||||
async rm() {
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
rm() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["rm", "-f", this.path_str()],
|
["rm", "-f", this.path_str()],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
|
proc.done((data) => {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
proc.fail((e, data) => {
|
proc.fail((e, data) => {
|
||||||
window.alert(data);
|
reject(data);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await proc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
show_properties() {
|
show_properties() {
|
||||||
@ -728,17 +866,28 @@ class NavContextMenu {
|
|||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
var functions = ["new_dir", "new_file", "new_link", "cut", "copy", "paste", "rename", "delete", "properties"];
|
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) {
|
for (let func of functions) {
|
||||||
var elem = document.createElement("div");
|
var elem = document.createElement("div");
|
||||||
var name_list = func.split("_");
|
var name_list = func[0].split("_");
|
||||||
name_list.forEach((word, index) => {name_list[index] = word.charAt(0).toUpperCase() + word.slice(1)});
|
name_list.forEach((word, index) => {name_list[index] = word.charAt(0).toUpperCase() + word.slice(1)});
|
||||||
elem.innerText = name_list.join(" ");
|
elem.innerHTML = func[1] + name_list.join(" ");
|
||||||
elem.addEventListener("click", (e) => {this[func].bind(this).apply()});
|
elem.addEventListener("click", (e) => {this[func[0]].bind(this).apply()});
|
||||||
elem.classList.add("nav-context-menu-item")
|
elem.classList.add("nav-context-menu-item")
|
||||||
elem.id = "nav-context-menu-" + func;
|
elem.id = "nav-context-menu-" + func[0];
|
||||||
this.dom_element.appendChild(elem);
|
this.dom_element.appendChild(elem);
|
||||||
this.menu_options[func] = elem;
|
this.menu_options[func[0]] = elem;
|
||||||
}
|
}
|
||||||
this.menu_options["paste"].style.display = "none";
|
this.menu_options["paste"].style.display = "none";
|
||||||
}
|
}
|
||||||
@ -770,7 +919,7 @@ class NavContextMenu {
|
|||||||
this.nav_window_ref.paste();
|
this.nav_window_ref.paste();
|
||||||
}
|
}
|
||||||
|
|
||||||
rename() {
|
async rename() {
|
||||||
this.hide();
|
this.hide();
|
||||||
var new_name = window.prompt("New Name: ", this.target.filename());
|
var new_name = window.prompt("New Name: ", this.target.filename());
|
||||||
if (new_name === null)
|
if (new_name === null)
|
||||||
@ -779,10 +928,55 @@ class NavContextMenu {
|
|||||||
window.alert("File name can't contain `/`.");
|
window.alert("File name can't contain `/`.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.target.mv(new_name);
|
try {
|
||||||
|
await this.target.mv(new_name);
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.nav_window_ref.refresh();
|
this.nav_window_ref.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zip_for_download() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var cmd = [
|
||||||
|
"/usr/share/cockpit/navigator/scripts/zip-for-download.py",
|
||||||
|
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.target instanceof NavDir)) {
|
||||||
|
download_target = this.target;
|
||||||
|
} else {
|
||||||
|
this.nav_window_ref.start_load();
|
||||||
|
var result;
|
||||||
|
try {
|
||||||
|
result = await this.zip_for_download();
|
||||||
|
} catch(e) {
|
||||||
|
this.nav_window_ref.stop_load();
|
||||||
|
window.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() {
|
delete() {
|
||||||
this.nav_window_ref.delete_selected();
|
this.nav_window_ref.delete_selected();
|
||||||
}
|
}
|
||||||
@ -803,25 +997,34 @@ class NavContextMenu {
|
|||||||
} else {
|
} else {
|
||||||
this.nav_window_ref.set_selected(target, false, false);
|
this.nav_window_ref.set_selected(target, false, false);
|
||||||
}
|
}
|
||||||
|
this.menu_options["download"].style.display = "flex";
|
||||||
if (target === this.nav_window_ref.pwd()) {
|
if (target === this.nav_window_ref.pwd()) {
|
||||||
this.menu_options["copy"].style.display = "none";
|
this.menu_options["copy"].style.display = "none";
|
||||||
this.menu_options["cut"].style.display = "none";
|
this.menu_options["cut"].style.display = "none";
|
||||||
this.menu_options["delete"].style.display = "none";
|
this.menu_options["delete"].style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
this.menu_options["copy"].style.display = "block";
|
this.menu_options["copy"].style.display = "flex";
|
||||||
this.menu_options["cut"].style.display = "block";
|
this.menu_options["cut"].style.display = "flex";
|
||||||
this.menu_options["delete"].style.display = "block";
|
this.menu_options["delete"].style.display = "flex";
|
||||||
}
|
}
|
||||||
if (this.nav_window_ref.selected_entries.size > 1) {
|
if (this.nav_window_ref.selected_entries.size > 1) {
|
||||||
this.menu_options["rename"].style.display = "none";
|
this.menu_options["rename"].style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
this.menu_options["rename"].style.display = "block";
|
this.menu_options["rename"].style.display = "flex";
|
||||||
|
if (target instanceof NavFileLink)
|
||||||
|
this.menu_options["download"].style.display = "none";
|
||||||
}
|
}
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.dom_element.style.display = "inline";
|
this.dom_element.style.display = "inline";
|
||||||
this.dom_element.style.left = event.clientX + "px";
|
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";
|
this.dom_element.style.top = event.clientY + "px";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
this.dom_element.style.display = "none";
|
this.dom_element.style.display = "none";
|
||||||
@ -847,6 +1050,7 @@ class FileUpload {
|
|||||||
this.reader = new FileReader();
|
this.reader = new FileReader();
|
||||||
this.chunks = this.slice_file(file);
|
this.chunks = this.slice_file(file);
|
||||||
this.chunk_index = 0;
|
this.chunk_index = 0;
|
||||||
|
this.timestamp = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
check_if_exists() {
|
check_if_exists() {
|
||||||
@ -860,20 +1064,40 @@ class FileUpload {
|
|||||||
make_html_element() {
|
make_html_element() {
|
||||||
var notification = document.createElement("div");
|
var notification = document.createElement("div");
|
||||||
notification.classList.add("nav-notification");
|
notification.classList.add("nav-notification");
|
||||||
|
|
||||||
var header = document.createElement("div");
|
var header = document.createElement("div");
|
||||||
header.classList.add("nav-notification-header");
|
header.classList.add("nav-notification-header");
|
||||||
notification.appendChild(header);
|
notification.appendChild(header);
|
||||||
header.innerText = "Uploading " + this.filename;
|
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");
|
var progress = document.createElement("progress");
|
||||||
progress.max = this.num_chunks;
|
progress.max = this.num_chunks;
|
||||||
notification.appendChild(progress);
|
notification.appendChild(progress);
|
||||||
this.progress = progress;
|
this.progress = progress;
|
||||||
this.html_elements = [progress, header, notification];
|
|
||||||
|
this.html_elements = [progress, eta, rate, info, header, notification];
|
||||||
document.getElementById("nav-notifications").appendChild(notification);
|
document.getElementById("nav-notifications").appendChild(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_html_element() {
|
remove_html_element() {
|
||||||
for (let elem of this.html_elements) {
|
for (let elem of this.html_elements) {
|
||||||
|
if (elem.parentElement)
|
||||||
elem.parentElement.removeChild(elem);
|
elem.parentElement.removeChild(elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -885,10 +1109,11 @@ class FileUpload {
|
|||||||
*/
|
*/
|
||||||
slice_file(file) {
|
slice_file(file) {
|
||||||
var offset = 0;
|
var offset = 0;
|
||||||
|
var next_offset;
|
||||||
var chunks = [];
|
var chunks = [];
|
||||||
this.num_chunks = Math.ceil(file.size / this.chunk_size);
|
this.num_chunks = Math.ceil(file.size / this.chunk_size);
|
||||||
for (let i = 0; i < this.num_chunks; i++) {
|
for (let i = 0; i < this.num_chunks; i++) {
|
||||||
var next_offset = Math.min(this.chunk_size * (i + 1), file.size);
|
next_offset = Math.min(this.chunk_size * (i + 1), file.size);
|
||||||
chunks.push(file.slice(offset, next_offset));
|
chunks.push(file.slice(offset, next_offset));
|
||||||
offset = next_offset;
|
offset = next_offset;
|
||||||
}
|
}
|
||||||
@ -897,7 +1122,7 @@ class FileUpload {
|
|||||||
|
|
||||||
async upload() {
|
async upload() {
|
||||||
if (await this.check_if_exists()) {
|
if (await this.check_if_exists()) {
|
||||||
window.alert(this.filename + ": File exists.");
|
if (!window.confirm(this.filename + ": File exists. Replace?"))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.make_html_element();
|
this.make_html_element();
|
||||||
@ -938,9 +1163,8 @@ class FileUpload {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Event} evt
|
* @param {Event} evt
|
||||||
* @param {Number} offset
|
|
||||||
*/
|
*/
|
||||||
write_to_file(evt, offset) {
|
write_to_file(evt) {
|
||||||
var chunk_b64 = this.arrayBufferToBase64(evt.target.result);
|
var chunk_b64 = this.arrayBufferToBase64(evt.target.result);
|
||||||
const seek = this.chunk_index * this.chunk_size;
|
const seek = this.chunk_index * this.chunk_size;
|
||||||
var obj = {
|
var obj = {
|
||||||
@ -948,6 +1172,7 @@ class FileUpload {
|
|||||||
chunk: chunk_b64
|
chunk: chunk_b64
|
||||||
};
|
};
|
||||||
this.proc.input(JSON.stringify(obj) + "\n", true);
|
this.proc.input(JSON.stringify(obj) + "\n", true);
|
||||||
|
this.update_xfr_rate();
|
||||||
}
|
}
|
||||||
|
|
||||||
done() {
|
done() {
|
||||||
@ -955,6 +1180,20 @@ class FileUpload {
|
|||||||
this.nav_window_ref.refresh();
|
this.nav_window_ref.refresh();
|
||||||
this.remove_html_element();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavDragDrop {
|
class NavDragDrop {
|
||||||
@ -992,7 +1231,7 @@ class NavDragDrop {
|
|||||||
window.alert(file.name + ": Cannot upload folders.");
|
window.alert(file.name + ": Cannot upload folders.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var uploader = new FileUpload(file, 4096, this.nav_window_ref);
|
var uploader = new FileUpload(file, 1048576, this.nav_window_ref);
|
||||||
uploader.upload();
|
uploader.upload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1000,7 +1239,7 @@ class NavDragDrop {
|
|||||||
for (let file of ev.dataTransfer.files) {
|
for (let file of ev.dataTransfer.files) {
|
||||||
if (file.type === "")
|
if (file.type === "")
|
||||||
continue;
|
continue;
|
||||||
var uploader = new FileUpload(file, 4096, this.nav_window_ref);
|
var uploader = new FileUpload(file, 1048576, this.nav_window_ref);
|
||||||
uploader.upload();
|
uploader.upload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1076,7 +1315,13 @@ class NavWindow {
|
|||||||
var bytes_sum = 0;
|
var bytes_sum = 0;
|
||||||
this.show_hidden = document.getElementById("nav-show-hidden").checked;
|
this.show_hidden = document.getElementById("nav-show-hidden").checked;
|
||||||
this.start_load();
|
this.start_load();
|
||||||
|
try {
|
||||||
var files = await this.pwd().get_children(this);
|
var files = await this.pwd().get_children(this);
|
||||||
|
} catch(e) {
|
||||||
|
this.up();
|
||||||
|
window.alert(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
while (this.entries.length) {
|
while (this.entries.length) {
|
||||||
var entry = this.entries.pop();
|
var entry = this.entries.pop();
|
||||||
entry.destroy();
|
entry.destroy();
|
||||||
@ -1126,9 +1371,7 @@ class NavWindow {
|
|||||||
this.path_stack.length = this.path_stack_index + 1;
|
this.path_stack.length = this.path_stack_index + 1;
|
||||||
this.path_stack.push(new_dir);
|
this.path_stack.push(new_dir);
|
||||||
this.path_stack_index = this.path_stack.length - 1;
|
this.path_stack_index = this.path_stack.length - 1;
|
||||||
this.refresh().catch(() => {
|
this.refresh();
|
||||||
this.back();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
@ -1164,6 +1407,9 @@ class NavWindow {
|
|||||||
var to_be_selected = [];
|
var to_be_selected = [];
|
||||||
if (append && this.selected_entries.has(entry)) {
|
if (append && this.selected_entries.has(entry)) {
|
||||||
this.selected_entries.delete(entry);
|
this.selected_entries.delete(entry);
|
||||||
|
if (this.selected_entries.size === 0) {
|
||||||
|
this.clear_selected();
|
||||||
|
}
|
||||||
} else if (select_range && this.last_selected_index !== -1) {
|
} else if (select_range && this.last_selected_index !== -1) {
|
||||||
var start = this.last_selected_index;
|
var start = this.last_selected_index;
|
||||||
var end = this.entries.indexOf(entry);
|
var end = this.entries.indexOf(entry);
|
||||||
@ -1333,10 +1579,18 @@ class NavWindow {
|
|||||||
new_owner !== entry.stat["owner"] ||
|
new_owner !== entry.stat["owner"] ||
|
||||||
new_group !== entry.stat["group"]
|
new_group !== entry.stat["group"]
|
||||||
) {
|
) {
|
||||||
await entry.chown(new_owner, new_group).catch(/*ignore, caught in chown*/);
|
try {
|
||||||
|
await entry.chown(new_owner, new_group);
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.changed_mode && (new_perms & 0o777) !== (entry.stat["mode"] & 0o777)) {
|
if (this.changed_mode && (new_perms & 0o777) !== (entry.stat["mode"] & 0o777)) {
|
||||||
await entry.chmod(new_perms).catch(/*ignore, caught in chmod*/);
|
try {
|
||||||
|
await entry.chmod(new_perms);
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
@ -1354,7 +1608,11 @@ class NavWindow {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let target of this.selected_entries) {
|
for (let target of this.selected_entries) {
|
||||||
await target.rm().catch(/*ignore, caught in rm*/);
|
try {
|
||||||
|
await target.rm();
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@ -1367,14 +1625,23 @@ class NavWindow {
|
|||||||
window.alert("Directory name can't contain `/`.");
|
window.alert("Directory name can't contain `/`.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var promise = new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["mkdir", this.pwd().path_str() + "/" + new_dir_name],
|
["mkdir", this.pwd().path_str() + "/" + new_dir_name],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
});
|
});
|
||||||
await proc;
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1386,14 +1653,23 @@ class NavWindow {
|
|||||||
window.alert("File name can't contain `/`.");
|
window.alert("File name can't contain `/`.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var promise = new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["/usr/share/cockpit/navigator/scripts/touch.py", this.pwd().path_str() + "/" + new_file_name],
|
["/usr/share/cockpit/navigator/scripts/touch.py", this.pwd().path_str() + "/" + new_file_name],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
});
|
});
|
||||||
await proc;
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1409,14 +1685,23 @@ class NavWindow {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var link_path = this.pwd().path_str() + "/" + link_name;
|
var link_path = this.pwd().path_str() + "/" + link_name;
|
||||||
|
var promise = new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
["ln", "-sn", link_target, link_path],
|
["ln", "-sn", link_target, link_path],
|
||||||
{superuser: "try", err: "out"}
|
{superuser: "try", err: "out"}
|
||||||
);
|
);
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert(data);
|
resolve();
|
||||||
});
|
});
|
||||||
await proc;
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1424,14 +1709,14 @@ class NavWindow {
|
|||||||
this.clip_board = [...this.selected_entries];
|
this.clip_board = [...this.selected_entries];
|
||||||
this.copy_or_move = "move";
|
this.copy_or_move = "move";
|
||||||
this.paste_cwd = this.pwd().path_str();
|
this.paste_cwd = this.pwd().path_str();
|
||||||
this.context_menu.menu_options["paste"].style.display = "block";
|
this.context_menu.menu_options["paste"].style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
copy() {
|
copy() {
|
||||||
this.clip_board = [...this.selected_entries];
|
this.clip_board = [...this.selected_entries];
|
||||||
this.copy_or_move = "copy";
|
this.copy_or_move = "copy";
|
||||||
this.paste_cwd = this.pwd().path_str();
|
this.paste_cwd = this.pwd().path_str();
|
||||||
this.context_menu.menu_options["paste"].style.display = "block";
|
this.context_menu.menu_options["paste"].style.display = "flex";
|
||||||
}
|
}
|
||||||
|
|
||||||
paste() {
|
paste() {
|
||||||
@ -1452,6 +1737,7 @@ class NavWindow {
|
|||||||
cmd.push(item.path_str());
|
cmd.push(item.path_str());
|
||||||
}
|
}
|
||||||
cmd.push(dest);
|
cmd.push(dest);
|
||||||
|
var promise = new Promise((resolve, reject) => {
|
||||||
var proc = cockpit.spawn(
|
var proc = cockpit.spawn(
|
||||||
cmd,
|
cmd,
|
||||||
{superuser: "try", err: "ignore"}
|
{superuser: "try", err: "ignore"}
|
||||||
@ -1465,13 +1751,20 @@ class NavWindow {
|
|||||||
window.alert(payload["message"]);
|
window.alert(payload["message"]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
proc.fail((e, data) => {
|
proc.done((data) => {
|
||||||
window.alert("Paste failed.");
|
resolve();
|
||||||
});
|
});
|
||||||
proc.always(() => {
|
proc.fail((e, data) => {
|
||||||
|
reject("Paste failed.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} catch(e) {
|
||||||
|
window.alert(e);
|
||||||
|
}
|
||||||
this.stop_load();
|
this.stop_load();
|
||||||
this.refresh();
|
this.refresh();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1506,10 +1799,12 @@ class NavWindow {
|
|||||||
return;
|
return;
|
||||||
this.nav_bar_last_parent_path_str = parent_path_str;
|
this.nav_bar_last_parent_path_str = parent_path_str;
|
||||||
var parent_dir = new NavDir(parent_path_str);
|
var parent_dir = new NavDir(parent_path_str);
|
||||||
var error = false;
|
var objs;
|
||||||
var objs = await parent_dir.get_children(this, true).catch(() => {error = true});
|
try {
|
||||||
if(error)
|
objs = await parent_dir.get_children(this);
|
||||||
|
} catch(e) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
objs = objs.filter((child) => {return child.nav_type === "dir"});
|
objs = objs.filter((child) => {return child.nav_type === "dir"});
|
||||||
while(list.firstChild)
|
while(list.firstChild)
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
@ -1554,13 +1849,27 @@ class NavWindow {
|
|||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_system_users() {
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
get_system_users() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
var proc = cockpit.spawn(["getent", "passwd"], {err: "ignore", superuser: "try"});
|
var proc = cockpit.spawn(["getent", "passwd"], {err: "ignore", superuser: "try"});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
var list = document.getElementById("possible-owners");
|
var list = document.getElementById("possible-owners");
|
||||||
while(list.firstChild) {
|
while(list.firstChild) {
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
}
|
}
|
||||||
var passwd = await proc;
|
var passwd;
|
||||||
|
try {
|
||||||
|
passwd = await proc;
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var passwd_entries = passwd.split("\n");
|
var passwd_entries = passwd.split("\n");
|
||||||
for (let entry of passwd_entries) {
|
for (let entry of passwd_entries) {
|
||||||
var cols = entry.split(":");
|
var cols = entry.split(":");
|
||||||
@ -1569,15 +1878,31 @@ class NavWindow {
|
|||||||
option.value = username;
|
option.value = username;
|
||||||
list.appendChild(option);
|
list.appendChild(option);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get_system_groups() {
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
get_system_groups() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
var proc = cockpit.spawn(["getent", "group"], {err: "ignore", superuser: "try"});
|
var proc = cockpit.spawn(["getent", "group"], {err: "ignore", superuser: "try"});
|
||||||
|
proc.fail((e, data) => {
|
||||||
|
reject(data);
|
||||||
|
});
|
||||||
var list = document.getElementById("possible-groups");
|
var list = document.getElementById("possible-groups");
|
||||||
while(list.firstChild) {
|
while(list.firstChild) {
|
||||||
list.removeChild(list.firstChild);
|
list.removeChild(list.firstChild);
|
||||||
}
|
}
|
||||||
var group = await proc;
|
var group
|
||||||
|
try {
|
||||||
|
group = await proc;
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var group_entries = group.split("\n");
|
var group_entries = group.split("\n");
|
||||||
for (let entry of group_entries) {
|
for (let entry of group_entries) {
|
||||||
var cols = entry.split(":");
|
var cols = entry.split(":");
|
||||||
@ -1586,6 +1911,8 @@ class NavWindow {
|
|||||||
option.value = groupname;
|
option.value = groupname;
|
||||||
list.appendChild(option);
|
list.appendChild(option);
|
||||||
}
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
disable_buttons_for_editing() {
|
disable_buttons_for_editing() {
|
||||||
@ -1604,6 +1931,7 @@ class NavWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select_all() {
|
select_all() {
|
||||||
|
this.selected_entries.clear();
|
||||||
for (let entry of this.entries) {
|
for (let entry of this.entries) {
|
||||||
if (!entry.is_hidden_file || this.show_hidden) {
|
if (!entry.is_hidden_file || this.show_hidden) {
|
||||||
this.set_selected(entry, false, true);
|
this.set_selected(entry, false, true);
|
||||||
|
@ -43,7 +43,7 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
path = sys.argv[1]
|
path = sys.argv[1]
|
||||||
try:
|
try:
|
||||||
file = open(path, "xb")
|
file = open(path, "wb")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
102
navigator/scripts/zip-for-download.py
Executable file
102
navigator/scripts/zip-for-download.py
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Cockpit Navigator - A File System Browser for Cockpit.
|
||||||
|
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
|
||||||
|
|
||||||
|
This file is part of Cockpit Navigator.
|
||||||
|
Cockpit Navigator is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
Cockpit Navigator is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
Synopsis: `zip-for-download.py </path/to/cwd> </path/to/file> [</path/to/file> ...]`
|
||||||
|
Output is JSON object with form:
|
||||||
|
{
|
||||||
|
message: <error message if applicable>,
|
||||||
|
archive-path: </path/to/archive>,
|
||||||
|
stat: {
|
||||||
|
size: <size of archive in bytes> // for setting channel max read size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def get_relpaths(full_paths, cwd):
|
||||||
|
response = []
|
||||||
|
for path in full_paths:
|
||||||
|
response.append(os.path.relpath(path, cwd))
|
||||||
|
return response
|
||||||
|
|
||||||
|
def make_zip(path):
|
||||||
|
try:
|
||||||
|
cwd = sys.argv[1]
|
||||||
|
files = get_relpaths(sys.argv[2:], cwd)
|
||||||
|
os.chdir(cwd)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({
|
||||||
|
"message": e
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
cmd = ["zip", "-ryq", path, *files]
|
||||||
|
try:
|
||||||
|
child = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({
|
||||||
|
"message": e
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
child.wait()
|
||||||
|
if child.returncode:
|
||||||
|
stdout, stderr = child.communicate()
|
||||||
|
print(json.dumps({
|
||||||
|
"message": stdout + stderr
|
||||||
|
}))
|
||||||
|
sys.exit(child.returncode)
|
||||||
|
try:
|
||||||
|
archive_size = os.stat(path).st_size
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({
|
||||||
|
"message": e
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
print(json.dumps({
|
||||||
|
"message": "",
|
||||||
|
"archive-path": path,
|
||||||
|
"stat": {
|
||||||
|
"size": archive_size
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
tmp_dir = "/tmp/navigator"
|
||||||
|
if not os.path.exists(tmp_dir):
|
||||||
|
os.mkdir(tmp_dir)
|
||||||
|
elif not os.path.isdir(tmp_dir):
|
||||||
|
print(json.dumps({
|
||||||
|
"message": "Temp path already exists."
|
||||||
|
}))
|
||||||
|
sys.exit(1)
|
||||||
|
archive_path = tmp_dir + "/navigator-download_" + datetime.now().strftime("%Y-%m-%d_%H-%M-%S.%f") + ".zip"
|
||||||
|
make_zip(archive_path)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user