Merge branch 'dev-josh'

This commit is contained in:
joshuaboud 2021-07-16 17:03:44 -03:00
commit f6a389e4f8
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
11 changed files with 231 additions and 79 deletions

View File

@ -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. * Implement uploading of entire directories.
* Moves focus to next input in modal popup when enter is pressed. * Add cancel option to in-progress file uploads.

View File

@ -23,17 +23,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.5.1/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.1-1focal_all.deb` 1. `# apt install ./cockpit-navigator_0.5.2-1focal_all.deb`
### EL7 ### 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 ### 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 ## From Source
1. Ensure dependencies are installed: `cockpit`, `python3`, `rsync`, `zip`. 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.5.1 is latest) 1. `$ git checkout <version>` (v0.5.2 is latest)
1. `# make install` 1. `# make install`
## From 45Drives Repositories ## From 45Drives Repositories
### Ubuntu ### Ubuntu

View File

@ -3,7 +3,7 @@
"name": "cockpit-navigator", "name": "cockpit-navigator",
"title": "Cockpit Navigator", "title": "Cockpit Navigator",
"prerelease": false, "prerelease": false,
"version": "0.5.1", "version": "0.5.2",
"buildVersion": "1", "buildVersion": "1",
"author": "Josh Boudreau <jboudreau@45drives.com>", "author": "Josh Boudreau <jboudreau@45drives.com>",
"url": "https://github.com/45Drives/cockpit-navigator", "url": "https://github.com/45Drives/cockpit-navigator",
@ -54,7 +54,7 @@
], ],
"changelog": { "changelog": {
"urgency": "medium", "urgency": "medium",
"version": "0.5.1", "version": "0.5.2",
"buildVersion": "1", "buildVersion": "1",
"ignore": [], "ignore": [],
"date": null, "date": null,

View File

@ -19,27 +19,31 @@
import {NavWindow} from "./NavWindow.js"; import {NavWindow} from "./NavWindow.js";
import {format_time_remaining} from "../functions.js"; import {format_time_remaining} from "../functions.js";
import {ModalPrompt} from "./ModalPrompt.js";
export class FileUpload { export class FileUpload {
/** /**
* *
* @param {File|Blob} file * @param {File|Blob} file
* @param {NavWindow} nav_window_ref * @param {NavWindow} nav_window_ref
* @param {string|undefined} path_prefix
*/ */
constructor(file, nav_window_ref) { constructor(file, nav_window_ref, path_prefix = "") {
try { try {
this.chunk_size = (parseInt(cockpit.info.version) > 238)? 1048576 : 65536; this.chunk_size = (parseInt(cockpit.info.version) > 238)? 1048576 : 65536;
} catch(e) { } catch(e) {
console.log(e); console.log(e);
this.chunk_size = 65536; this.chunk_size = 65536;
} }
this.filename = file.name; this.filename = path_prefix + file.name;
this.nav_window_ref = nav_window_ref; 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.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(); this.timestamp = Date.now();
this.modal_prompt = new ModalPrompt();
this.using_webkit = true;
} }
check_if_exists() { check_if_exists() {
@ -58,6 +62,21 @@ export class FileUpload {
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;
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"); var info = document.createElement("div");
info.classList.add("flex-row", "space-between"); info.classList.add("flex-row", "space-between");
@ -94,7 +113,7 @@ export class FileUpload {
/** /**
* *
* @param {File|Blob} file * @param {File|Blob} file
* @returns {Array} * @returns {Blob[]}
*/ */
slice_file(file) { slice_file(file) {
var offset = 0; var offset = 0;
@ -110,35 +129,48 @@ export class FileUpload {
} }
async upload() { 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.make_html_element();
this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py3", this.path], {err: "out", superuser: "try"}); this.proc = cockpit.spawn(["/usr/share/cockpit/navigator/scripts/write-chunks.py3", this.path], {err: "out", superuser: "try"});
this.proc.fail((e, data) => { this.proc.fail((e, data) => {
this.reader.onload = () => {} this.reader.onload = () => {}
this.done(); this.done();
this.nav_window_ref.modal_prompt.alert(data); this.nav_window_ref.modal_prompt.alert(e, data);
}) })
this.proc.done((data) => { this.proc.done((data) => {
this.nav_window_ref.refresh(); this.nav_window_ref.refresh();
}) })
this.reader.onload = (function(uploader_ref) { this.reader.onerror = (evt) => {
return async function(evt) { this.modal_prompt.alert("Failed to read file: " + this.filename, "Upload of directories not supported.");
uploader_ref.write_to_file(evt, uploader_ref.chunk_index * uploader_ref.chunk_size); this.done();
uploader_ref.chunk_index++; }
uploader_ref.progress.value = uploader_ref.chunk_index; this.reader.onload = (evt) => {
if (uploader_ref.chunk_index < uploader_ref.num_chunks) this.write_to_file(evt, this.chunk_index * this.chunk_size);
uploader_ref.reader.readAsArrayBuffer(uploader_ref.chunks[uploader_ref.chunk_index]); this.chunk_index++;
else { this.progress.value = this.chunk_index;
uploader_ref.done(); if (this.chunk_index < this.num_chunks)
} this.reader.readAsArrayBuffer(this.chunks[this.chunk_index]);
}; else {
})(this); this.done();
this.reader.readAsArrayBuffer(this.chunks[0]); }
};
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) { arrayBufferToBase64(buffer) {
let binary = ''; let binary = '';
let bytes = new Uint8Array(buffer); let bytes = new Uint8Array(buffer);

View File

@ -111,6 +111,7 @@ export class ModalPrompt {
this.footer.innerHTML = ""; this.footer.innerHTML = "";
this.footer.appendChild(this.ok); this.footer.appendChild(this.ok);
this.show(); this.show();
this.ok.focus();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.ok.onclick = () => { this.ok.onclick = () => {
resolve(); resolve();
@ -137,6 +138,10 @@ export class ModalPrompt {
else else
this.yes.classList.add(primary_btn); this.yes.classList.add(primary_btn);
this.show(); this.show();
if (danger)
this.no.focus();
else
this.yes.focus();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let resolve_true = () => { let resolve_true = () => {
resolve(true); resolve(true);
@ -179,7 +184,7 @@ export class ModalPrompt {
let label = document.createElement("label"); let label = document.createElement("label");
label.innerText = request.label; label.innerText = request.label;
label.htmlFor = key; label.htmlFor = key;
label.style.marginRight = "1em"; label.style.paddingRight = "1em";
label.style.flexBasis = "0"; label.style.flexBasis = "0";
label.style.flexGrow = "1"; label.style.flexGrow = "1";
let req = document.createElement("input"); let req = document.createElement("input");
@ -196,6 +201,9 @@ export class ModalPrompt {
case "text": case "text":
req.style.flexGrow = "3"; req.style.flexGrow = "3";
break; break;
case "checkbox":
label.style.cursor = req.style.cursor = "pointer";
break;
default: default:
break; break;
} }

View File

@ -18,6 +18,7 @@
*/ */
import {FileUpload} from "./FileUpload.js"; import {FileUpload} from "./FileUpload.js";
import { ModalPrompt } from "./ModalPrompt.js";
import {NavWindow} from "./NavWindow.js"; import {NavWindow} from "./NavWindow.js";
export class NavDragDrop { export class NavDragDrop {
@ -27,65 +28,148 @@ export class NavDragDrop {
* @param {NavWindow} nav_window_ref * @param {NavWindow} nav_window_ref
*/ */
constructor(drop_area, nav_window_ref) { constructor(drop_area, nav_window_ref) {
drop_area.addEventListener("dragenter", this); drop_area.addEventListener("dragenter", this, false);
drop_area.addEventListener("dragover", this); drop_area.addEventListener("dragover", this, false);
drop_area.addEventListener("dragleave", this); drop_area.addEventListener("dragleave", this, false);
drop_area.addEventListener("drop", this); drop_area.addEventListener("drop", this, false);
this.drop_area = drop_area; this.drop_area = drop_area;
this.nav_window_ref = nav_window_ref; 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){ switch(e.type){
case "dragenter": case "dragenter":
e.preventDefault();
e.stopPropagation();
this.drop_area.classList.add("drag-enter"); this.drop_area.classList.add("drag-enter");
break; break;
case "dragover": case "dragover":
e.preventDefault();
e.stopPropagation();
break; break;
case "dragleave": case "dragleave":
e.preventDefault();
e.stopPropagation();
this.drop_area.classList.remove("drag-enter"); this.drop_area.classList.remove("drag-enter");
break; break;
case "drop": case "drop":
if (e.dataTransfer.items) { let uploads;
for (let item of e.dataTransfer.items) { let items = e.dataTransfer.items;
if (item.kind === 'file') { e.preventDefault();
var file = item.getAsFile(); e.stopPropagation();
if (file.type === "" && file.size !== 0) { try {
this.nav_window_ref.modal_prompt.alert(file.name + ": Cannot upload folders."); uploads = await this.handle_drop_advanced(items);
continue; } catch {
} uploads = [];
if (file.size === 0) { for (let file of e.dataTransfer.files) {
var proc = cockpit.spawn( let uploader = new FileUpload(file, this.nav_window_ref);
["/usr/share/cockpit/navigator/scripts/touch.py3", this.nav_window_ref.pwd().path_str() + "/" + file.name], uploader.using_webkit = false;
{superuser: "try", err: "out"} uploads.push(uploader);
);
proc.done(() => {
this.nav_window_ref.refresh();
});
proc.fail((e, data) => {
this.nav_window_ref.modal_prompt.alert(data);
});
} else {
var uploader = new FileUpload(file, this.nav_window_ref);
uploader.upload();
}
}
}
} else {
for (let file of ev.dataTransfer.files) {
if (file.type === "")
continue;
var uploader = new FileUpload(file, this.nav_window_ref);
uploader.upload();
} }
} }
this.drop_area.classList.remove("drag-enter"); this.drop_area.classList.remove("drag-enter");
if (uploads.length === 0)
break;
uploads = await this.handle_conflicts(uploads);
uploads.forEach((upload) => {upload.upload()});
break; break;
default: default:
this.drop_area.classList.remove("drag-enter"); this.drop_area.classList.remove("drag-enter");
break; break;
} }
e.stopPropagation();
} }
} }

View File

@ -35,6 +35,7 @@
--nav-border-radius: 4px; --nav-border-radius: 4px;
--symlink-symbol-color: var(--navigation); --symlink-symbol-color: var(--navigation);
--list-view-header: var(--selected); --list-view-header: var(--selected);
--outline-color: black;
} }
[data-theme="dark"] { [data-theme="dark"] {
@ -53,6 +54,11 @@
--nav-entry-color: #555F6E; --nav-entry-color: #555F6E;
--symlink-symbol-color: var(--navigation); --symlink-symbol-color: var(--navigation);
--list-view-header: var(--container); --list-view-header: var(--container);
--outline-color: white;
}
button {
outline-color: var(--outline-color) !important;
} }
html { html {

View File

@ -32,27 +32,35 @@ import sys
import json import json
def write_chunk(chunk, file): 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"] seek = chunk["seek"]
data = base64.b64decode(chunk["chunk"]) data = base64.b64decode(chunk["chunk"])
file.seek(seek) file.seek(seek)
file.write(data) file.write(data)
def main(): def main():
file = None
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Invalid number of arguments.") print("Invalid number of arguments.")
sys.exit(1) sys.exit(1)
path = sys.argv[1]
try:
file = open(path, "wb")
except Exception as e:
print(e)
sys.exit(1)
while True: while True:
try: try:
json_in = input() json_in = input()
except EOFError: except EOFError:
break 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: for json_obj in json_list:
try: try:
obj_in = json.loads(json_obj) obj_in = json.loads(json_obj)
@ -63,7 +71,8 @@ def main():
log.close() log.close()
sys.exit(1) sys.exit(1)
write_chunk(obj_in, file) write_chunk(obj_in, file)
file.close() if file:
file.close()
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -32,6 +32,9 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/* /usr/share/cockpit/navigator/*
%changelog %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 * Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.1-1
- Allow modal popups to scroll if overflowing past page. - Allow modal popups to scroll if overflowing past page.
- Moves focus to next input in modal popup when enter is pressed. - Moves focus to next input in modal popup when enter is pressed.

View File

@ -32,6 +32,9 @@ rm -rf %{buildroot}
/usr/share/cockpit/navigator/* /usr/share/cockpit/navigator/*
%changelog %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 * Thu Jul 15 2021 Josh Boudreau <jboudreau@45drives.com> 0.5.1-1
- Allow modal popups to scroll if overflowing past page. - Allow modal popups to scroll if overflowing past page.
- Moves focus to next input in modal popup when enter is pressed. - Moves focus to next input in modal popup when enter is pressed.

View File

@ -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 cockpit-navigator (0.5.1-1focal) focal; urgency=medium
* Allow modal popups to scroll if overflowing past page. * Allow modal popups to scroll if overflowing past page.