import {NavEntry} from "./NavEntry.js"; import {NavDir} from "./NavDir.js"; import {NavContextMenu} from "./NavContextMenu.js"; import {NavDragDrop} from "./NavDragDrop.js"; import {SortFunctions} from "./SortFunctions.js"; import {ModalPrompt} from "./ModalPrompt.js"; import {format_bytes, format_permissions} from "../functions.js"; export class NavWindow { constructor() { this.item_display = "grid"; this.path_stack = (localStorage.getItem('navigator-path') ?? '/').split('/'); this.path_stack = this.path_stack.map((_, index) => new NavDir([...this.path_stack.slice(0, index + 1)].filter(part => part != ''), this)); this.path_stack_index = this.path_stack.length - 1; this.selected_entries = new Set([this.pwd()]); this.entries = []; this.window = document.getElementById("nav-contents-view"); this.window.addEventListener("click", this); this.window.addEventListener("contextmenu", this); window.addEventListener("keydown", this); this.last_selected_index = -1; this.context_menu = new NavContextMenu("nav-context-menu", this); this.clip_board = []; this.uploader = new NavDragDrop(this.window, this); this.sort_function = new SortFunctions(); this.modal_prompt = new ModalPrompt(); } /** * * @param {Event} e */ handleEvent(e) { switch (e.type) { case "click": this.clear_selected(); this.show_selected_properties(); break; case "contextmenu": this.context_menu.show(e, this.pwd()); e.preventDefault(); break; case "keydown": if (e.keyCode === 46 && e.target === document.body) { this.delete_selected(); } else if (e.keyCode === 65 && e.ctrlKey) { this.select_all(); e.preventDefault(); } else if (e.keyCode === 67 && e.ctrlKey) { this.copy(); } else if (e.keyCode === 86 && e.ctrlKey) { this.paste(); } else if (e.keyCode === 88 && e.ctrlKey) { this.cut(); } break; default: break; } } async refresh() { localStorage.setItem('navigator-path', `/${this.path_stack[this.path_stack_index].path.join('/')}`); var num_dirs = 0; var num_files = 0; var bytes_sum = 0; this.show_hidden = document.getElementById("nav-show-hidden").checked; this.start_load(); try { var files = await this.pwd().get_children(this); } catch(e) { this.up(); this.modal_prompt.alert(e); return; } while (this.entries.length) { var entry = this.entries.pop(); entry.destroy(); } files.sort((first, second) => { if (first.nav_type === second.nav_type) { return this.item_display === "list" ? this.sort_function.get_func()(first, second) : this.sort_function.name_asc(first, second); // default to sort by name in grid view } if (first.nav_type === "dir") return -1; return 1; }); files.forEach((file) => { if (file.nav_type === "dir") num_dirs++; else { num_files++; bytes_sum += file.stat["size"]; } if(!file.is_hidden_file || this.show_hidden) { this.window.appendChild(file.dom_element); this.entries.push(file); } file.context_menu_ref = this.context_menu; }); document.getElementById("pwd").value = this.pwd().path_str(); this.set_selected(this.pwd(), false, false); this.show_selected_properties(); document.getElementById("nav-num-dirs").innerText = num_dirs.toString(); document.getElementById("nav-num-files").innerText = num_files.toString(); document.getElementById("nav-num-bytes").innerText = format_bytes(bytes_sum); this.stop_load(); this.set_nav_button_state(); } set_nav_button_state() { document.getElementById("nav-back-btn").disabled = (this.path_stack_index === 1); document.getElementById("nav-forward-btn").disabled = (this.path_stack_index === this.path_stack.length - 1); document.getElementById("nav-up-dir-btn").disabled = (this.pwd().path_str() === "/"); } /** * * @returns {NavDir} */ pwd() { return this.path_stack[this.path_stack_index]; } /** * * @param {NavDir} new_dir */ cd(new_dir) { this.path_stack.length = this.path_stack_index + 1; this.path_stack.push(new_dir); this.path_stack_index = this.path_stack.length - 1; this.refresh(); } back() { this.path_stack_index = Math.max(this.path_stack_index - 1, 0); this.refresh(); } forward() { this.path_stack_index = Math.min(this.path_stack_index + 1, this.path_stack.length - 1); this.refresh(); } up() { if(this.pwd().path_str() !== '/') this.cd(new NavDir(this.pwd().parent_dir())); } /** * * @param {NavEntry} entry * @param {Boolean} select_range * @param {Boolean} append */ set_selected(entry, select_range, append) { this.hide_edit_selected(); for (let i of this.selected_entries) { i.dom_element.classList.remove("nav-item-selected"); if (i.nav_type === "dir") { i.dom_element.nav_item_icon.classList.remove("fa-folder-open"); i.dom_element.nav_item_icon.classList.add("fa-folder"); } } var to_be_selected = []; if (append && this.selected_entries.has(entry)) { this.selected_entries.delete(entry); if (this.selected_entries.size === 0) { this.clear_selected(); } } else if (select_range && this.last_selected_index !== -1) { var start = this.last_selected_index; var end = this.entries.indexOf(entry); if (end < start) [start, end] = [end, start]; if (end === -1) return; to_be_selected = this.entries.slice(start, end + 1).filter(entry => !entry.stat["inaccessible"]); } else { if (!append) this.selected_entries.clear(); to_be_selected = [entry]; } for (let i of to_be_selected) { this.selected_entries.add(i); } for (let i of this.selected_entries) { i.dom_element.classList.add("nav-item-selected"); if (i.nav_type === "dir") { i.dom_element.nav_item_icon.classList.remove("fa-folder"); i.dom_element.nav_item_icon.classList.add("fa-folder-open"); } } if (this.selected_entries.size > 1){ var name_fields = document.getElementsByClassName("nav-info-column-filename"); for (let name_field of name_fields) { name_field.innerText = this.selected_entries.size.toString() + " selected" name_field.title = name_field.innerText; } document.getElementById("nav-info-column-properties").innerHTML = ""; } else { this.show_selected_properties(); } this.last_selected_index = this.entries.indexOf(entry); } clear_selected() { this.set_selected(this.pwd(), false, false); } selected_entry() { return [...this.selected_entries][this.selected_entries.size - 1]; } none_selected() { return this.selected_entries.size === 1 && this.selected_entry() === this.pwd(); } show_selected_properties() { this.selected_entry().show_properties(); } async show_edit_selected() { var dangerous_dirs = [ "/", "/usr", "/bin", "/sbin", "/lib", "/lib32", "/lib64", "/usr/bin", "/usr/include", "/usr/lib", "/usr/lib32", "/usr/lib64", "/usr/sbin", ]; var dangerous_selected = []; for (let i of this.selected_entries) { var path = i.path_str(); if (dangerous_dirs.includes(path)) { dangerous_selected.push(path); } } if (dangerous_selected.length > 0) { var dangerous_selected_str = ""; if (dangerous_selected.length > 2) { var last = dangerous_selected.pop(); dangerous_selected_str = dangerous_selected.join(", "); dangerous_selected_str += ", and " + last; } else if (dangerous_selected.length === 2) { dangerous_selected_str = dangerous_selected.join(" and "); } else { dangerous_selected_str = dangerous_selected[0]; } if (!await this.modal_prompt.confirm( "Warning: editing " + dangerous_selected_str + " can be dangerous.", "Are you sure?", true )) { return; } } else if (this.selected_entries.size > 1) { if (!await this.modal_prompt.confirm( "Warning: editing permissions for " + this.selected_entries.size + " files.", "Are you sure?", true )) { return; } } if (this.selected_entries.size === 1) { this.selected_entry().populate_edit_fields(); document.getElementById("selected-files-list-header").innerText = ""; document.getElementById("selected-files-list").innerText = ""; } else { for (let field of ["owner", "group"]) { document.getElementById("nav-edit-" + field).value = ""; } var filename = document.getElementById("nav-edit-filename"); filename.value = "N/A"; filename.disabled = true; for (let checkbox of document.getElementsByClassName("mode-checkbox")) { checkbox.checked = false; } var targets = []; for (let target of this.selected_entries) { targets.push(target.filename()); } var targets_str = targets.join(", "); document.getElementById("selected-files-list-header").innerText = "Applying edits to:"; document.getElementById("selected-files-list").innerText = targets_str; } this.update_permissions_preview(); this.changed_mode = false; document.getElementById("nav-mode-preview").innerText = "unchanged"; document.getElementById("nav-edit-properties").style.display = "flex"; document.getElementById("nav-show-properties").style.display = "none"; } hide_edit_selected() { document.getElementById("nav-show-properties").style.display = "flex"; document.getElementById("nav-edit-properties").style.display = "none"; } /** * * @returns {number} */ get_new_permissions() { var category_list = ["other", "group", "owner"]; var action_list = ["exec", "write", "read"]; var result = 0; var bit = 0; for (let category of category_list) { for (let action of action_list) { if (document.getElementById(category + "-" + action).checked) result |= 1 << bit; bit++; } } return result; } update_permissions_preview() { var new_perms = this.get_new_permissions(); var text = format_permissions(new_perms); text += " (" + (new_perms & 0o777).toString(8) + ")"; document.getElementById("nav-mode-preview").innerText = text; this.changed_mode = true; } async apply_edit_selected() { var new_owner = document.getElementById("nav-edit-owner").value; var new_group = document.getElementById("nav-edit-group").value; var new_perms = this.get_new_permissions(); for (let entry of this.selected_entries) { if ( new_owner !== entry.stat["owner"] || new_group !== entry.stat["group"] ) { try { await entry.chown(new_owner, new_group); } catch(e) { this.modal_prompt.alert(e); } } if (this.changed_mode && (new_perms & 0o777) !== (entry.stat["mode"] & 0o777)) { try { await entry.chmod(new_perms); } catch(e) { this.modal_prompt.alert(e); } } } this.refresh(); this.hide_edit_selected(); } async delete_selected() { var prompt = ""; if (this.selected_entries.size > 1) { prompt = "Deleting " + this.selected_entries.size + " files."; } else { prompt = "Deleting `" + this.selected_entry().path_str() + "`."; } if (!await this.modal_prompt.confirm(prompt, "This cannot be undone. Are you sure?", true)) { return; } for (let target of this.selected_entries) { try { await target.rm(); } catch(e) { this.modal_prompt.alert(e); } } this.refresh(); } async mkdir() { let response = await this.modal_prompt.prompt("Creating Directory", { new_name: { label: "Name: ", type: "text" } } ); if (response === null) return; var new_dir_name = response.new_name; if (new_dir_name === "") { this.modal_prompt.alert("Directory name can't be empty."); return; } if (new_dir_name.includes("/")) { this.modal_prompt.alert("Directory name can't contain `/`."); return; } var promise = new Promise((resolve, reject) => { var proc = cockpit.spawn( ["mkdir", this.pwd().path_str() + "/" + new_dir_name], {superuser: "try", err: "out"} ); proc.done((data) => { resolve(); }); proc.fail((e, data) => { reject(data); }); }); try { await promise; } catch(e) { this.modal_prompt.alert(e); } this.refresh(); } async touch() { let response = await this.modal_prompt.prompt("Creating File", { new_name: { label: "Name: ", type: "text" } } ); if (response === null) return; var new_file_name = response.new_name; if (new_file_name === "") { this.modal_prompt.alert("File name can't be empty."); return; } if (new_file_name.includes("/")) { this.modal_prompt.alert("File name can't contain `/`."); return; } var promise = new Promise((resolve, reject) => { var proc = cockpit.spawn( ["/usr/share/cockpit/navigator/scripts/touch.py", this.pwd().path_str() + "/" + new_file_name], {superuser: "try", err: "out"} ); proc.done((data) => { resolve(); }); proc.fail((e, data) => { reject(data); }); }); try { await promise; } catch(e) { this.modal_prompt.alert(e); } this.refresh(); } async ln(default_target = "") { let response = await this.modal_prompt.prompt("Creating Symbolic Link", { target: { label: "Target: ", type: "text", default: default_target }, name: { label: "Name: ", type: "text" } } ); if (response === null) return; var link_target = response.target; if (link_target === "") { this.modal_prompt.alert("Link target can't be empty."); return; } var link_name = response.name; if (link_name === "") { this.modal_prompt.alert("Link name can't be empty."); return; } if (link_name.includes("/")) { this.modal_prompt.alert("Link name can't contain `/`."); return; } var link_path = this.pwd().path_str() + "/" + link_name; var promise = new Promise((resolve, reject) => { var proc = cockpit.spawn( ["ln", "-sn", link_target, link_path], {superuser: "try", err: "out"} ); proc.done((data) => { resolve(); }); proc.fail((e, data) => { reject(data); }); }); try { await promise; } catch(e) { this.modal_prompt.alert(e); } this.refresh(); } cut() { this.clip_board = [...this.selected_entries]; this.copy_or_move = "move"; this.paste_cwd = this.pwd().path_str(); this.context_menu.menu_options["paste"].style.display = "flex"; } copy() { this.clip_board = [...this.selected_entries]; this.copy_or_move = "copy"; this.paste_cwd = this.pwd().path_str(); this.context_menu.menu_options["paste"].style.display = "flex"; } paste() { this.paste_clipboard(); this.context_menu.hide_paste(); } async paste_clipboard() { this.start_load(); var cmd = ["/usr/share/cockpit/navigator/scripts/paste.py"]; var dest = this.pwd().path_str(); if (this.copy_or_move === "move") { cmd.push("-m"); } cmd.push(this.paste_cwd); for (let item of this.clip_board) { cmd.push(item.path_str()); } cmd.push(dest); this.clip_board.length = 0; // clear clipboard var promise = new Promise((resolve, reject) => { var proc = cockpit.spawn( cmd, {superuser: "try", err: "ignore"} ); proc.stream(async (data) => { var payload = JSON.parse(data); if (payload["wants-response"]) { if (payload.hasOwnProperty("conflicts")) { let requests = {}; for (let conflict of payload["conflicts"]) { requests[conflict[0]] = { label: conflict[1], type: "checkbox", default: false } } this.stop_load(); let responses = await this.modal_prompt.prompt("Overwrite?", requests); this.start_load(); if (responses === null) { proc.input(JSON.stringify("abort") + "\n"); return; } let keepers = []; for (let response of Object.keys(responses)) { if (responses[response]) keepers.push(response) } proc.input(JSON.stringify(keepers) + "\n", true); } else { var user_response = await this.modal_prompt.confirm(payload["message"]); proc.input(JSON.stringify(user_response) + "\n", true); } } else { await this.modal_prompt.alert(payload["message"]); } }); proc.done((data) => { resolve(); }); proc.fail((e, data) => { reject("Paste failed."); }); }); try { await promise; } catch(e) { this.modal_prompt.alert(e); } this.stop_load(); this.refresh(); } /** * * @param {Event} e */ nav_bar_event_handler(e) { switch(e.key){ case 'Enter': this.nav_bar_cd(); break; default: break; } } nav_bar_cd() { var new_path = document.getElementById("pwd").value; while (new_path.charAt(new_path.length - 1) === '/' && new_path.length > 1) new_path = new_path.substr(0, new_path.length - 1); this.cd(new NavDir(new_path)); } async nav_bar_update_choices() { var list = document.getElementById("possible-paths"); var partial_path_str = document.getElementById("pwd").value; var last_delim = partial_path_str.lastIndexOf('/'); if(last_delim === -1) return; var parent_path_str = partial_path_str.slice(0, last_delim); if(this.nav_bar_last_parent_path_str === parent_path_str) return; this.nav_bar_last_parent_path_str = parent_path_str; var parent_dir = new NavDir(parent_path_str); var objs; try { objs = await parent_dir.get_children(this); } catch(e) { return; } objs = objs.filter((child) => {return child.nav_type === "dir"}); while(list.firstChild) list.removeChild(list.firstChild); objs.forEach((obj) => { var option = document.createElement("option"); option.value = obj.path_str(); list.appendChild(option); }); } start_load() { document.getElementById("nav-loader-container").style.display = "block"; var buttons = document.getElementsByTagName("button"); for (let button of buttons) { button.disabled = true; } } stop_load() { document.getElementById("nav-loader-container").style.display = "none"; var buttons = document.getElementsByTagName("button"); for (let button of buttons) { button.disabled = false; } } /** * * @param {Event} e */ toggle_show_hidden(e) { localStorage.setItem('show-hidden-files', e.target.checked); var icon = document.getElementById("nav-show-hidden-icon"); if (e.target.checked) { icon.classList.remove("fa-low-vision"); icon.classList.add("fa-eye"); } else { icon.classList.remove("fa-eye"); icon.classList.add("fa-low-vision"); } this.refresh(); } /** * * @returns {Promise} */ get_system_users() { return new Promise(async (resolve, reject) => { var proc = cockpit.spawn(["getent", "passwd"], {err: "ignore", superuser: "try"}); proc.fail((e, data) => { reject(data); }); var list = document.getElementById("possible-owners"); while(list.firstChild) { list.removeChild(list.firstChild); } var passwd; try { passwd = await proc; } catch(e) { reject(e); return; } var passwd_entries = passwd.split("\n"); passwd_entries.sort((first, second) => { return first.split(":")[0].localeCompare(second.split(":")[0]); }); for (let entry of passwd_entries) { var cols = entry.split(":"); var username = cols[0]; var option = document.createElement("option"); option.value = username; list.appendChild(option); } resolve(); }); } /** * * @returns {Promise} */ get_system_groups() { return new Promise(async (resolve, reject) => { var proc = cockpit.spawn(["getent", "group"], {err: "ignore", superuser: "try"}); proc.fail((e, data) => { reject(data); }); var list = document.getElementById("possible-groups"); while(list.firstChild) { list.removeChild(list.firstChild); } var group try { group = await proc; } catch(e) { reject(e); return; } var group_entries = group.split("\n"); group_entries.sort((first, second) => { return first.split(":")[0].localeCompare(second.split(":")[0]); }); for (let entry of group_entries) { var cols = entry.split(":"); var groupname = cols[0]; var option = document.createElement("option"); option.value = groupname; list.appendChild(option); } resolve(); }); } disable_buttons_for_editing() { for (let button of document.getElementsByTagName("button")) { if (!button.classList.contains("editor-btn")) button.disabled = true; } document.getElementById("pwd").disabled = true; } enable_buttons() { for (let button of document.getElementsByTagName("button")) { button.disabled = false; } document.getElementById("pwd").disabled = false; this.set_nav_button_state(); } select_all() { this.selected_entries.clear(); for (let entry of this.entries) { if (!entry.is_hidden_file || this.show_hidden) { this.set_selected(entry, false, true); } } } async switch_item_display() { var button = document.getElementById("nav-item-display-icon"); if (this.item_display === "grid") { this.item_display = "list"; await this.refresh(); this.window.classList.remove("contents-view-grid"); this.window.classList.add("contents-view-list"); button.classList.remove("fa-list"); button.classList.add("fa-th"); } else { this.item_display = "grid"; await this.refresh(); this.window.classList.remove("contents-view-list"); this.window.classList.add("contents-view-grid"); button.classList.remove("fa-th"); button.classList.add("fa-list"); } localStorage.setItem("item-display", this.item_display); } search_filter(event) { var search_name = event.target.value; this.entries.forEach((entry) => { if (entry.filename().toLowerCase().startsWith(search_name.toLowerCase())) entry.show(); else entry.hide(); }); } }