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.
* 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.

View File

@ -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

View File

@ -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,

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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__":

View File

@ -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.

View File

@ -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.

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
* Allow modal popups to scroll if overflowing past page.