Merge branch 'dev-josh-paste-overhaul' into dev-josh

This commit is contained in:
joshuaboud 2021-11-29 12:59:37 -04:00
commit dabffa2853
No known key found for this signature in database
GPG Key ID: 17EFB59E2A8BF50E
2 changed files with 137 additions and 97 deletions

View File

@ -645,6 +645,7 @@ export class NavWindow {
cmd.push(item.path_str()); cmd.push(item.path_str());
} }
cmd.push(dest); cmd.push(dest);
console.log(cmd);
this.clip_board.length = 0; // clear clipboard this.clip_board.length = 0; // clear clipboard
var promise = new Promise((resolve, reject) => { var promise = new Promise((resolve, reject) => {
var proc = cockpit.spawn( var proc = cockpit.spawn(
@ -670,24 +671,21 @@ export class NavWindow {
proc.input(JSON.stringify("abort") + "\n"); proc.input(JSON.stringify("abort") + "\n");
return; return;
} }
let keepers = []; proc.input(JSON.stringify(responses) + "\n", true);
for (let response of Object.keys(responses)) {
if (responses[response])
keepers.push(response)
}
proc.input(JSON.stringify(keepers) + "\n", true);
} else { } else {
var user_response = await this.modal_prompt.confirm(payload["message"]); var user_response = await this.modal_prompt.confirm(payload["message"]);
proc.input(JSON.stringify(user_response) + "\n", true); proc.input(JSON.stringify(user_response) + "\n", true);
} }
} else { } else {
await this.modal_prompt.alert(payload["message"]); await this.modal_prompt.alert(payload["message"], payload?.detail);
} }
}); });
proc.done((data) => { proc.done((data) => {
resolve(); resolve();
}); });
proc.fail((e, data) => { proc.fail((e, data) => {
console.log(e);
console.log(data);
reject("Paste failed."); reject("Paste failed.");
}); });
}); });

View File

@ -1,20 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Cockpit Navigator - A File System Browser for Cockpit. Cockpit Navigator - A File System Browser for Cockpit.
Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com> Copyright (C) 2021 Josh Boudreau <jboudreau@45drives.com>
This file is part of Cockpit Navigator. This file is part of Cockpit Navigator.
Cockpit Navigator is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
Cockpit Navigator is distributed in the hope that it will be useful, Cockpit Navigator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>. along with Cockpit Navigator. If not, see <https://www.gnu.org/licenses/>.
""" """
""" """
@ -22,93 +22,135 @@ Synopsis: `paste.py3 [-m] <cwd of copy> <list of source files> <destination dire
all full paths all full paths
""" """
import os from functools import partial
import os, shutil, errno
import sys import sys
from optparse import OptionParser from optparse import OptionParser
import json import json
import subprocess
def prompt_user(message, wants_response, conflicts = None): class File:
payload = { def __init__(self, path, cwd, dest_path):
"wants-response": wants_response, self.src = path
"message": message self.dst = dest_path + "/" + os.path.relpath(path, cwd)
}
if conflicts != None:
payload["conflicts"] = conflicts
print(json.dumps(payload) + "\n")
if wants_response:
response = json.loads(input())
if isinstance(response, str) and response == "abort":
sys.exit(0)
return response
return
def split_paths_at_cwd(paths, cwd): def __str__(self):
response = [] return self.src + " -> " + self.dst
for path in paths:
response.append(cwd + "/./" + os.path.relpath(path, cwd)) def __repr__(self):
return response return "File(" + self.__str__() + ")"
def check_if_exists(self):
return os.path.exists(self.dst)
def recursive_get_conflicts(input_array, cwd, dest): def move(self):
conflicts = [] if self.check_if_exists(): # for overwriting
non_conflicts = [] os.remove(self.dst)
for source in input_array: if not os.path.exists(os.path.dirname(self.dst)):
if os.path.isdir(source): os.makedirs(os.path.dirname(self.dst))
child_nodes = os.listdir(source) shutil.move(self.src, self.dst, copy_function=partial(shutil.copy2, follow_symlinks=False))
child_paths = []
for node in child_nodes: def copy(self):
child_paths.append(source + "/" + node) if self.check_if_exists(): # for overwriting
(more_conflicts, more_non_conflicts) = recursive_get_conflicts(child_paths, cwd, dest) os.remove(self.dst)
conflicts += more_conflicts if not os.path.exists(os.path.dirname(self.dst)):
non_conflicts += more_non_conflicts os.makedirs(os.path.dirname(self.dst))
continue shutil.copy2(self.src, self.dst, follow_symlinks=False)
dest_path = dest + "/" + os.path.relpath(source, cwd)
if os.path.exists(dest_path):
conflicts.append((source, dest_path))
else:
non_conflicts.append(source)
return (conflicts, non_conflicts)
def filter_existing(args, cwd): def prompt_user(message, wants_response, conflicts = None, detail = None):
sources = args[:-1] payload = {
dest = args[-1] "wants-response": wants_response,
(conflicts, non_conflicts) = recursive_get_conflicts(sources, cwd, dest) "message": message
if len(conflicts): }
conflicts = prompt_user("Overwrite?", True, conflicts) if conflicts != None:
non_conflicts.extend(conflicts) payload["conflicts"] = conflicts
if not len(non_conflicts): if detail != None:
sys.exit(0) # exit if nothing to copy payload["detail"] = detail
filtered_args = [*split_paths_at_cwd(non_conflicts, cwd), dest] print(json.dumps(payload) + "\n")
return filtered_args if wants_response:
response = json.loads(input())
if isinstance(response, str) and response == "abort":
sys.exit(0)
return response
return
def paste(cmd, args): def get_conflicts(files):
try: conflicts = []
child = subprocess.Popen( non_conflicts = []
[*cmd, *args], for file in files:
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True if file.check_if_exists():
) conflicts.append(file)
except Exception as e: else:
prompt_user(str(e), False) non_conflicts.append(file)
sys.exit(1) return (conflicts, non_conflicts)
child.wait()
if child.returncode:
stdout, stderr = child.communicate()
prompt_user(stdout + stderr, False)
sys.exit(child.returncode)
def filter_existing(files):
(conflicts, non_conflicts) = get_conflicts(files)
if len(conflicts):
prompts = list(map(lambda f: [f.src, f.dst], conflicts)) # get list of [src, dst]
keeper_lut = prompt_user("Overwrite?", True, prompts) # returns dict of srcs : keep (bool)
non_conflicts.extend(filter(lambda f: keeper_lut[f.src], conflicts))
return non_conflicts
def recursive_get_files(cwd, dest_path, source_directory):
files = []
directories_to_remove = []
for entry in os.listdir(source_directory):
path = source_directory + "/" + entry
if os.path.isdir(path):
(new_files, new_directories) = recursive_get_files(cwd, dest_path, path)
files.extend(new_files)
directories_to_remove.extend(new_directories)
directories_to_remove.append(path)
else:
files.append(File(path, cwd, dest_path))
return (files, directories_to_remove)
def main(): def main():
parser = OptionParser() parser = OptionParser()
parser.add_option("-m", "--move", help="remove source files", action="store_true", dest="move", default=False) parser.add_option("-m", "--move", help="remove source files", action="store_true", dest="move", default=False)
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
cwd = args[0] cwd = args[0]
filtered_args = filter_existing(args[1:], cwd) sources = args[1:-1]
if options.move: dest_path = args[-1]
paste(["rsync", "-aI", "--relative", "--remove-source-files"], filtered_args) files = []
else: directories_to_remove = []
paste(["rsync", "-aI", "--relative"], filtered_args) for source_path in sources:
sys.exit(0) if os.path.isdir(source_path):
(new_files, new_directories) = recursive_get_files(cwd, dest_path, source_path)
files.extend(new_files)
directories_to_remove.extend(new_directories)
directories_to_remove.append(source_path)
elif os.path.exists(source_path):
files.append(File(source_path, cwd, dest_path))
files = filter(lambda f: f.src != f.dst, files)
files = filter_existing(files)
if not len(files):
sys.exit(0) # exit if nothing to copy
if options.move:
for file in files:
try:
file.move()
except Exception as e:
prompt_user("Failed to move " + os.path.relpath(file.src, cwd), wants_response=False, detail=str(e))
for directory in directories_to_remove:
try:
os.rmdir(directory)
except OSError as e:
if e.errno == errno.ENOTEMPTY:
pass # skip deletion
else:
prompt_user("Failed to remove directory " + directory, wants_response=False, detail=str(e))
except Exception as e:
prompt_user("Failed to remove directory " + directory, wants_response=False, detail=str(e))
else:
for file in files:
try:
file.copy()
except Exception as e:
prompt_user("Failed to copy " + os.path.relpath(file.src, cwd), wants_response=False, detail=str(e))
prompt_user("Directories to remove:\n" + "\n".join(directories_to_remove), False)
sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
main() main()