mirror of
https://github.com/45Drives/cockpit-navigator.git
synced 2025-07-29 08:34:50 +02:00
Merge branch 'dev-josh'
This commit is contained in:
commit
f6a389e4f8
@ -1,4 +1,4 @@
|
||||
## Cockpit Navigator 0.5.1-1
|
||||
## Cockpit Navigator 0.5.2-1
|
||||
|
||||
* Allow modal popups to scroll if overflowing past page.
|
||||
* Moves focus to next input in modal popup when enter is pressed.
|
||||
* Implement uploading of entire directories.
|
||||
* Add cancel option to in-progress file uploads.
|
10
README.md
10
README.md
@ -23,17 +23,17 @@ With no command line use needed, you can:
|
||||
# Installation
|
||||
## From Github Release
|
||||
### Ubuntu
|
||||
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.1/cockpit-navigator_0.5.1-1focal_all.deb`
|
||||
1. `# apt install ./cockpit-navigator_0.5.1-1focal_all.deb`
|
||||
1. `$ wget https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.2/cockpit-navigator_0.5.2-1focal_all.deb`
|
||||
1. `# apt install ./cockpit-navigator_0.5.2-1focal_all.deb`
|
||||
### EL7
|
||||
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.1/cockpit-navigator-0.5.1-1.el7.noarch.rpm`
|
||||
1. `# yum install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.2/cockpit-navigator-0.5.2-1.el7.noarch.rpm`
|
||||
### EL8
|
||||
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.1/cockpit-navigator-0.5.1-1.el8.noarch.rpm`
|
||||
1. `# dnf install https://github.com/45Drives/cockpit-navigator/releases/download/v0.5.2/cockpit-navigator-0.5.2-1.el8.noarch.rpm`
|
||||
## From Source
|
||||
1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`, `zip`.
|
||||
1. `$ git clone https://github.com/45Drives/cockpit-navigator.git`
|
||||
1. `$ cd cockpit-navigator`
|
||||
1. `$ git checkout <version>` (v0.5.1 is latest)
|
||||
1. `$ git checkout <version>` (v0.5.2 is latest)
|
||||
1. `# make install`
|
||||
## From 45Drives Repositories
|
||||
### Ubuntu
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "cockpit-navigator",
|
||||
"title": "Cockpit Navigator",
|
||||
"prerelease": false,
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"buildVersion": "1",
|
||||
"author": "Josh Boudreau <jboudreau@45drives.com>",
|
||||
"url": "https://github.com/45Drives/cockpit-navigator",
|
||||
@ -54,7 +54,7 @@
|
||||
],
|
||||
"changelog": {
|
||||
"urgency": "medium",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"buildVersion": "1",
|
||||
"ignore": [],
|
||||
"date": null,
|
||||
|
@ -19,27 +19,31 @@
|
||||
|
||||
import {NavWindow} from "./NavWindow.js";
|
||||
import {format_time_remaining} from "../functions.js";
|
||||
import {ModalPrompt} from "./ModalPrompt.js";
|
||||
|
||||
export class FileUpload {
|
||||
/**
|
||||
*
|
||||
* @param {File|Blob} file
|
||||
* @param {NavWindow} nav_window_ref
|
||||
* @param {string|undefined} path_prefix
|
||||
*/
|
||||
constructor(file, nav_window_ref) {
|
||||
constructor(file, nav_window_ref, path_prefix = "") {
|
||||
try {
|
||||
this.chunk_size = (parseInt(cockpit.info.version) > 238)? 1048576 : 65536;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
this.chunk_size = 65536;
|
||||
}
|
||||
this.filename = file.name;
|
||||
this.filename = path_prefix + file.name;
|
||||
this.nav_window_ref = nav_window_ref;
|
||||
this.path = nav_window_ref.pwd().path_str() + "/" + file.name;
|
||||
this.path = nav_window_ref.pwd().path_str() + "/" + this.filename;
|
||||
this.reader = new FileReader();
|
||||
this.chunks = this.slice_file(file);
|
||||
this.chunk_index = 0;
|
||||
this.timestamp = Date.now();
|
||||
this.modal_prompt = new ModalPrompt();
|
||||
this.using_webkit = true;
|
||||
}
|
||||
|
||||
check_if_exists() {
|
||||
@ -58,6 +62,21 @@ export class FileUpload {
|
||||
header.classList.add("nav-notification-header");
|
||||
notification.appendChild(header);
|
||||
header.innerText = "Uploading " + this.filename;
|
||||
header.style.position = "relative";
|
||||
header.style.paddingRight = "1em";
|
||||
|
||||
var cancel = document.createElement("i");
|
||||
cancel.classList.add("fa", "fa-times");
|
||||
cancel.style.position = "absolute"
|
||||
cancel.style.right = "0";
|
||||
cancel.style.cursor = "pointer";
|
||||
cancel.onclick = () => {
|
||||
if (this.proc) {
|
||||
this.reader.onload = () => {};
|
||||
this.done();
|
||||
}
|
||||
}
|
||||
header.appendChild(cancel);
|
||||
|
||||
var info = document.createElement("div");
|
||||
info.classList.add("flex-row", "space-between");
|
||||
@ -94,7 +113,7 @@ export class FileUpload {
|
||||
/**
|
||||
*
|
||||
* @param {File|Blob} file
|
||||
* @returns {Array}
|
||||
* @returns {Blob[]}
|
||||
*/
|
||||
slice_file(file) {
|
||||
var offset = 0;
|
||||
@ -110,35 +129,48 @@ export class FileUpload {
|
||||
}
|
||||
|
||||
async upload() {
|
||||
if (await this.check_if_exists()) {
|
||||
if (!await this.nav_window_ref.modal_prompt.confirm(this.filename + ": File exists. Replace?", "", true))
|
||||
return;
|
||||
}
|
||||
this.make_html_element();
|
||||
this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py3", this.path], {err: "out", superuser: "try"});
|
||||
this.proc.fail((e, data) => {
|
||||
this.reader.onload = () => {}
|
||||
this.done();
|
||||
this.nav_window_ref.modal_prompt.alert(data);
|
||||
this.nav_window_ref.modal_prompt.alert(e, data);
|
||||
})
|
||||
this.proc.done((data) => {
|
||||
this.nav_window_ref.refresh();
|
||||
})
|
||||
this.reader.onload = (function(uploader_ref) {
|
||||
return async function(evt) {
|
||||
uploader_ref.write_to_file(evt, uploader_ref.chunk_index * uploader_ref.chunk_size);
|
||||
uploader_ref.chunk_index++;
|
||||
uploader_ref.progress.value = uploader_ref.chunk_index;
|
||||
if (uploader_ref.chunk_index < uploader_ref.num_chunks)
|
||||
uploader_ref.reader.readAsArrayBuffer(uploader_ref.chunks[uploader_ref.chunk_index]);
|
||||
else {
|
||||
uploader_ref.done();
|
||||
}
|
||||
};
|
||||
})(this);
|
||||
this.reader.readAsArrayBuffer(this.chunks[0]);
|
||||
this.reader.onerror = (evt) => {
|
||||
this.modal_prompt.alert("Failed to read file: " + this.filename, "Upload of directories not supported.");
|
||||
this.done();
|
||||
}
|
||||
this.reader.onload = (evt) => {
|
||||
this.write_to_file(evt, this.chunk_index * this.chunk_size);
|
||||
this.chunk_index++;
|
||||
this.progress.value = this.chunk_index;
|
||||
if (this.chunk_index < this.num_chunks)
|
||||
this.reader.readAsArrayBuffer(this.chunks[this.chunk_index]);
|
||||
else {
|
||||
this.done();
|
||||
}
|
||||
};
|
||||
try {
|
||||
this.reader.readAsArrayBuffer(this.chunks[0]);
|
||||
} catch {
|
||||
this.reader.onload = () => {};
|
||||
if (this.using_webkit) {
|
||||
this.proc.input(JSON.stringify({seek: 0, chunk: ""}), true);
|
||||
} else {
|
||||
this.modal_prompt.alert("Failed to read file: " + this.filename, "Upload of directories and empty files not supported.");
|
||||
}
|
||||
this.done();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ArrayBuffer} buffer
|
||||
* @returns
|
||||
*/
|
||||
arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
|
@ -111,6 +111,7 @@ export class ModalPrompt {
|
||||
this.footer.innerHTML = "";
|
||||
this.footer.appendChild(this.ok);
|
||||
this.show();
|
||||
this.ok.focus();
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ok.onclick = () => {
|
||||
resolve();
|
||||
@ -137,6 +138,10 @@ export class ModalPrompt {
|
||||
else
|
||||
this.yes.classList.add(primary_btn);
|
||||
this.show();
|
||||
if (danger)
|
||||
this.no.focus();
|
||||
else
|
||||
this.yes.focus();
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolve_true = () => {
|
||||
resolve(true);
|
||||
@ -179,7 +184,7 @@ export class ModalPrompt {
|
||||
let label = document.createElement("label");
|
||||
label.innerText = request.label;
|
||||
label.htmlFor = key;
|
||||
label.style.marginRight = "1em";
|
||||
label.style.paddingRight = "1em";
|
||||
label.style.flexBasis = "0";
|
||||
label.style.flexGrow = "1";
|
||||
let req = document.createElement("input");
|
||||
@ -196,6 +201,9 @@ export class ModalPrompt {
|
||||
case "text":
|
||||
req.style.flexGrow = "3";
|
||||
break;
|
||||
case "checkbox":
|
||||
label.style.cursor = req.style.cursor = "pointer";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import {FileUpload} from "./FileUpload.js";
|
||||
import { ModalPrompt } from "./ModalPrompt.js";
|
||||
import {NavWindow} from "./NavWindow.js";
|
||||
|
||||
export class NavDragDrop {
|
||||
@ -27,65 +28,148 @@ export class NavDragDrop {
|
||||
* @param {NavWindow} nav_window_ref
|
||||
*/
|
||||
constructor(drop_area, nav_window_ref) {
|
||||
drop_area.addEventListener("dragenter", this);
|
||||
drop_area.addEventListener("dragover", this);
|
||||
drop_area.addEventListener("dragleave", this);
|
||||
drop_area.addEventListener("drop", this);
|
||||
drop_area.addEventListener("dragenter", this, false);
|
||||
drop_area.addEventListener("dragover", this, false);
|
||||
drop_area.addEventListener("dragleave", this, false);
|
||||
drop_area.addEventListener("drop", this, false);
|
||||
this.drop_area = drop_area;
|
||||
this.nav_window_ref = nav_window_ref;
|
||||
this.modal_prompt = new ModalPrompt();
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
e.preventDefault();
|
||||
/**
|
||||
*
|
||||
* @param {FileSystemEntry} item
|
||||
* @param {string} path
|
||||
* @returns {Promise<FileUpload[]>}
|
||||
*/
|
||||
async scan_files(item, path) {
|
||||
let new_uploads = [];
|
||||
if (item.isDirectory) {
|
||||
if (!path && !await this.modal_prompt.confirm(`Copy whole directory: ${item.fullPath}?`, "", true))
|
||||
return new_uploads;
|
||||
let directoryReader = item.createReader();
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
directoryReader.readEntries(async (entries) => {
|
||||
for (const entry of entries) {
|
||||
new_uploads.push(... await this.scan_files(entry, path + item.name + "/"));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
await promise;
|
||||
} else {
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
item.file((file) => {
|
||||
resolve(file);
|
||||
})
|
||||
});
|
||||
new_uploads.push(new FileUpload(await promise, this.nav_window_ref, path));
|
||||
}
|
||||
return new_uploads;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DataTransferItemList} items
|
||||
* @returns {Promise<DataTransferItemList>}
|
||||
*/
|
||||
handle_drop_advanced(items) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let uploads = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let item = items[i]?.webkitGetAsEntry?.() ?? items[i]?.getAsEntry?.() ?? null;
|
||||
let path = "";
|
||||
if (item) {
|
||||
let new_uploads = await this.scan_files(item, path);
|
||||
console.log(new_uploads);
|
||||
uploads.push(... new_uploads);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
resolve(uploads);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FileUpload[]} uploads
|
||||
* @returns {FileUpload[]}
|
||||
*/
|
||||
async handle_conflicts(uploads) {
|
||||
let keepers = [];
|
||||
let requests = {};
|
||||
for (let upload of uploads) {
|
||||
if (!await upload.check_if_exists()) {
|
||||
keepers.push(upload.filename);
|
||||
continue;
|
||||
}
|
||||
let request = {};
|
||||
request.label = upload.filename;
|
||||
request.type = "checkbox";
|
||||
let id = upload.filename;
|
||||
requests[id] = request;
|
||||
}
|
||||
if (Object.keys(requests).length > 0) {
|
||||
let responses = await this.nav_window_ref.modal_prompt.prompt(
|
||||
"Conflicts found while uploading. Replace?",
|
||||
requests
|
||||
)
|
||||
if (responses === null)
|
||||
return null;
|
||||
for (let key of Object.keys(responses)) {
|
||||
if (responses[key])
|
||||
keepers.push(key);
|
||||
}
|
||||
}
|
||||
return uploads.filter((upload) => keepers.includes(upload.filename));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async handleEvent(e) {
|
||||
switch(e.type){
|
||||
case "dragenter":
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.drop_area.classList.add("drag-enter");
|
||||
break;
|
||||
case "dragover":
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
break;
|
||||
case "dragleave":
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.drop_area.classList.remove("drag-enter");
|
||||
break;
|
||||
case "drop":
|
||||
if (e.dataTransfer.items) {
|
||||
for (let item of e.dataTransfer.items) {
|
||||
if (item.kind === 'file') {
|
||||
var file = item.getAsFile();
|
||||
if (file.type === "" && file.size !== 0) {
|
||||
this.nav_window_ref.modal_prompt.alert(file.name + ": Cannot upload folders.");
|
||||
continue;
|
||||
}
|
||||
if (file.size === 0) {
|
||||
var proc = cockpit.spawn(
|
||||
["/usr/share/cockpit/navigator/scripts/touch.py3", this.nav_window_ref.pwd().path_str() + "/" + file.name],
|
||||
{superuser: "try", err: "out"}
|
||||
);
|
||||
proc.done(() => {
|
||||
this.nav_window_ref.refresh();
|
||||
});
|
||||
proc.fail((e, data) => {
|
||||
this.nav_window_ref.modal_prompt.alert(data);
|
||||
});
|
||||
} else {
|
||||
var uploader = new FileUpload(file, this.nav_window_ref);
|
||||
uploader.upload();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let file of ev.dataTransfer.files) {
|
||||
if (file.type === "")
|
||||
continue;
|
||||
var uploader = new FileUpload(file, this.nav_window_ref);
|
||||
uploader.upload();
|
||||
let uploads;
|
||||
let items = e.dataTransfer.items;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
uploads = await this.handle_drop_advanced(items);
|
||||
} catch {
|
||||
uploads = [];
|
||||
for (let file of e.dataTransfer.files) {
|
||||
let uploader = new FileUpload(file, this.nav_window_ref);
|
||||
uploader.using_webkit = false;
|
||||
uploads.push(uploader);
|
||||
}
|
||||
}
|
||||
this.drop_area.classList.remove("drag-enter");
|
||||
if (uploads.length === 0)
|
||||
break;
|
||||
uploads = await this.handle_conflicts(uploads);
|
||||
uploads.forEach((upload) => {upload.upload()});
|
||||
break;
|
||||
default:
|
||||
this.drop_area.classList.remove("drag-enter");
|
||||
break;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@
|
||||
--nav-border-radius: 4px;
|
||||
--symlink-symbol-color: var(--navigation);
|
||||
--list-view-header: var(--selected);
|
||||
--outline-color: black;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@ -53,6 +54,11 @@
|
||||
--nav-entry-color: #555F6E;
|
||||
--symlink-symbol-color: var(--navigation);
|
||||
--list-view-header: var(--container);
|
||||
--outline-color: white;
|
||||
}
|
||||
|
||||
button {
|
||||
outline-color: var(--outline-color) !important;
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -32,27 +32,35 @@ import sys
|
||||
import json
|
||||
|
||||
def write_chunk(chunk, file):
|
||||
if not file:
|
||||
path = sys.argv[1]
|
||||
parent_path = os.path.dirname(path)
|
||||
if not os.path.exists(parent_path):
|
||||
os.makedirs(parent_path, exist_ok=True)
|
||||
elif os.path.isfile(parent_path):
|
||||
print(parent_path + ": exists and is not a directory.")
|
||||
sys.exit(1)
|
||||
try:
|
||||
file = open(path, "wb")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
seek = chunk["seek"]
|
||||
data = base64.b64decode(chunk["chunk"])
|
||||
file.seek(seek)
|
||||
file.write(data)
|
||||
|
||||
def main():
|
||||
file = None
|
||||
if len(sys.argv) != 2:
|
||||
print("Invalid number of arguments.")
|
||||
sys.exit(1)
|
||||
path = sys.argv[1]
|
||||
try:
|
||||
file = open(path, "wb")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
while True:
|
||||
try:
|
||||
json_in = input()
|
||||
except EOFError:
|
||||
break
|
||||
json_list = json_in.split("\n")
|
||||
json_list = json_in.split("\n") # need to split in case writes happen faster than reads
|
||||
for json_obj in json_list:
|
||||
try:
|
||||
obj_in = json.loads(json_obj)
|
||||
@ -63,7 +71,8 @@ def main():
|
||||
log.close()
|
||||
sys.exit(1)
|
||||
write_chunk(obj_in, file)
|
||||
file.close()
|
||||
if file:
|
||||
file.close()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -32,6 +32,9 @@ rm -rf %{buildroot}
|
||||
/usr/share/cockpit/navigator/*
|
||||
|
||||
%changelog
|
||||
* Fri Jul 16 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.2-1
|
||||
- Implement uploading of entire directories.
|
||||
- Add cancel option to in-progress file uploads.
|
||||
* Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.1-1
|
||||
- Allow modal popups to scroll if overflowing past page.
|
||||
- Moves focus to next input in modal popup when enter is pressed.
|
||||
|
@ -32,6 +32,9 @@ rm -rf %{buildroot}
|
||||
/usr/share/cockpit/navigator/*
|
||||
|
||||
%changelog
|
||||
* Fri Jul 16 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.2-1
|
||||
- Implement uploading of entire directories.
|
||||
- Add cancel option to in-progress file uploads.
|
||||
* Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.1-1
|
||||
- Allow modal popups to scroll if overflowing past page.
|
||||
- Moves focus to next input in modal popup when enter is pressed.
|
||||
|
@ -1,3 +1,10 @@
|
||||
cockpit-navigator (0.5.2-1focal) focal; urgency=medium
|
||||
|
||||
* Implement uploading of entire directories.
|
||||
* Add cancel option to in-progress file uploads.
|
||||
|
||||
-- Josh Boudreau <jboudreau@45drives.com> Fri, 16 Jul 2021 13:56:55 -0300
|
||||
|
||||
cockpit-navigator (0.5.1-1focal) focal; urgency=medium
|
||||
|
||||
* Allow modal popups to scroll if overflowing past page.
|
||||
|
Loading…
x
Reference in New Issue
Block a user