diff options
-rw-r--r-- | ranger/core/actions.py | 47 | ||||
-rw-r--r-- | ranger/core/fm.py | 15 | ||||
-rw-r--r-- | ranger/core/loader.py | 83 | ||||
-rw-r--r-- | ranger/ext/shutil_generatorized.py | 302 | ||||
-rw-r--r-- | ranger/fsobject/directory.py | 3 |
5 files changed, 114 insertions, 336 deletions
diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 4b3752ef..2e775848 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -29,8 +29,7 @@ from ranger import fsobject from ranger.core.shared import FileManagerAware, EnvironmentAware, \ SettingsAware from ranger.fsobject import File -from ranger.ext import shutil_generatorized as shutil_g -from ranger.core.loader import LoadableObject +from ranger.core.loader import CommandLoader class _MacroTemplate(string.Template): """A template for substituting macros in commands""" @@ -685,11 +684,12 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): if not copied_files: return + def refresh(_): + cwd = self.env.get_directory(original_path) + cwd.load_content() + original_path = self.env.cwd.path - try: - one_file = copied_files[0] - except: - one_file = None + one_file = copied_files[0] if self.env.cut: self.env.copy.clear() @@ -698,36 +698,21 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): descr = "moving: " + one_file.path else: descr = "moving files from: " + one_file.dirname - def generate(): - for f in copied_files: - for _ in shutil_g.move(src=f.path, - dst=original_path, - overwrite=overwrite): - yield - cwd = self.env.get_directory(original_path) - cwd.load_content() + obj = CommandLoader(args=['mv', '--backup=existing', + '--suffix=_', '-ft', self.env.cwd.path] + \ + [f.path for f in copied_files], + descr=descr, end_hook=refresh) else: if len(copied_files) == 1: descr = "copying: " + one_file.path else: descr = "copying files from: " + one_file.dirname - def generate(): - for f in self.env.copy: - if isdir(f.path): - for _ in shutil_g.copytree(src=f.path, - dst=join(original_path, f.basename), - symlinks=True, - overwrite=overwrite): - yield - else: - for _ in shutil_g.copy2(f.path, original_path, - symlinks=True, - overwrite=overwrite): - yield - cwd = self.env.get_directory(original_path) - cwd.load_content() - - self.loader.add(LoadableObject(generate(), descr)) + obj = CommandLoader(args=['cp', '--backup=existing', '--archive', + '--suffix=_', '-frt', self.env.cwd.path] + \ + [f.path for f in self.env.copy], + descr=descr, end_hook=refresh) + + self.loader.add(obj) def delete(self): self.notify("Deleting!", duration=1) diff --git a/ranger/core/fm.py b/ranger/core/fm.py index 5e01f96f..cd1c73b6 100644 --- a/ranger/core/fm.py +++ b/ranger/core/fm.py @@ -97,6 +97,21 @@ class FM(Actions, SignalDispatcher): mimetypes.knownfiles.append(self.relpath('data/mime.types')) self.mimetypes = mimetypes.MimeTypes() + def destroy(self): + debug = ranger.arg.debug + if self.ui: + try: + self.ui.destroy() + except: + if debug: + raise + if self.loader: + try: + self.loader.destroy() + except: + if debug: + raise + def block_input(self, sec=0): self.input_blocked = sec != 0 self.input_blocked_until = time() + sec diff --git a/ranger/core/loader.py b/ranger/core/loader.py index cda578f2..8272d0e0 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -14,11 +14,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from collections import deque +from time import time, sleep +from subprocess import Popen, PIPE from time import time -from ranger.core.shared import FileManagerAware +from ranger.shared import FileManagerAware import math +import os +import select -class LoadableObject(object): + +class Loadable(object): + paused = False def __init__(self, gen, descr): self.load_generator = gen self.description = descr @@ -26,6 +32,69 @@ class LoadableObject(object): def get_description(self): return self.description + def pause(self): + self.paused = True + + def unpause(self): + try: + del self.paused + except: + pass + + def destroy(self): + pass + + +class CommandLoader(Loadable, FileManagerAware): + """ + Run an external command with the loader. + + Output from stderr will be reported. Ensure that the process doesn't + ever ask for input, otherwise the loader will be blocked until this + object is removed from the queue (type ^C in ranger) + """ + finished = False + def __init__(self, args, descr, begin_hook=None, end_hook=None): + Loadable.__init__(self, self.generate(), descr) + self.args = args + self.begin_hook = begin_hook + self.end_hook = end_hook + + def generate(self): + self.process = process = Popen(self.args, + stdout=open(os.devnull, 'w'), + stderr=PIPE) + if self.begin_hook: + self.begin_hook(process) + while process.poll() is None: + try: + rd, _, __ = select.select( + [process.stderr], [], [], 0.05) + if rd: + error = process.stderr.readline().decode('utf-8') + self.fm.notify(error, bad=True) + except select.error: + pass + sleep(0.02) + yield + self.finished = True + if self.end_hook: + self.end_hook(process) + + def pause(self): + if not self.finished and not self.paused: + self.process.send_signal(20) + Loadable.pause(self) + + def unpause(self): + if not self.finished and self.paused: + self.process.send_signal(18) + Loadable.unpause(self) + + def destroy(self): + if self.process: + self.process.kill() + class Loader(FileManagerAware): seconds_of_work_time = 0.03 @@ -66,6 +135,8 @@ class Loader(FileManagerAware): if to == 0: self.queue.appendleft(item) + if _from != 0: + self.queue[1].pause() elif to == -1: self.queue.append(item) else: @@ -85,6 +156,7 @@ class Loader(FileManagerAware): item = self.queue[index] if hasattr(item, 'unload'): item.unload() + item.destroy() del self.queue[index] def work(self): @@ -105,7 +177,10 @@ class Loader(FileManagerAware): self.rotate() if item != self.old_item: + if self.old_item: + self.old_item.pause() self.old_item = item + item.unpause() end_time = time() + self.seconds_of_work_time @@ -121,3 +196,7 @@ class Loader(FileManagerAware): def has_work(self): """Is there anything to load?""" return bool(self.queue) + + def destroy(self): + while self.queue: + self.queue.pop().destroy() diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py deleted file mode 100644 index 436d2cb7..00000000 --- a/ranger/ext/shutil_generatorized.py +++ /dev/null @@ -1,302 +0,0 @@ -# This file was taken from the python standard library and has been -# slightly modified to do a "yield" after every 16KB of copying -"""Utility functions for copying files and directory trees. - -XXX The functions here don't copy the resource fork or other metadata on Mac. - -""" - -import os -import sys -import stat -from os.path import abspath - -__all__ = ["copyfileobj","copyfile","copystat","copy2", - "copytree","move","rmtree","Error", "SpecialFileError"] - -APPENDIX = '_' - -class Error(EnvironmentError): - pass - -class SpecialFileError(EnvironmentError): - """Raised when trying to do a kind of operation (e.g. copying) which is - not supported on a special file (e.g. a named pipe)""" - -try: - WindowsError -except NameError: - WindowsError = None - -def copyfileobj(fsrc, fdst, length=16*1024): - """copy data from file-like object fsrc to file-like object fdst""" - while 1: - buf = fsrc.read(length) - if not buf: - break - fdst.write(buf) - yield - -def _samefile(src, dst): - # Macintosh, Unix. - if hasattr(os.path,'samefile'): - try: - return os.path.samefile(src, dst) - except OSError: - return False - - # All other platforms: check for same pathname. - return (os.path.normcase(abspath(src)) == - os.path.normcase(abspath(dst))) - -def copyfile(src, dst): - """Copy data from src to dst""" - if _samefile(src, dst): - raise Error("`%s` and `%s` are the same file" % (src, dst)) - - fsrc = None - fdst = None - for fn in [src, dst]: - try: - st = os.stat(fn) - except OSError: - # File most likely does not exist - pass - else: - # XXX What about other special files? (sockets, devices...) - if stat.S_ISFIFO(st.st_mode): - raise SpecialFileError("`%s` is a named pipe" % fn) - try: - fsrc = open(src, 'rb') - fdst = open(dst, 'wb') - for _ in copyfileobj(fsrc, fdst): - yield - finally: - if fdst: - fdst.close() - if fsrc: - fsrc.close() - -def copystat(src, dst): - """Copy all stat info (mode bits, atime, mtime, flags) from src to dst""" - st = os.stat(src) - mode = stat.S_IMODE(st.st_mode) - if hasattr(os, 'utime'): - try: os.utime(dst, (st.st_atime, st.st_mtime)) - except: pass - if hasattr(os, 'chmod'): - try: os.chmod(dst, mode) - except: pass - if hasattr(os, 'chflags') and hasattr(st, 'st_flags'): - try: os.chflags(dst, st.st_flags) - except: pass - -def copy2(src, dst, overwrite=False, symlinks=False): - """Copy data and all stat info ("cp -p src dst"). - - The destination may be a directory. - - """ - if os.path.isdir(dst): - dst = os.path.join(dst, os.path.basename(src)) - if not overwrite: - dst = get_safe_path(dst) - if symlinks and os.path.islink(src): - linkto = os.readlink(src) - os.symlink(linkto, dst) - else: - for _ in copyfile(src, dst): - yield - copystat(src, dst) - -def get_safe_path(dst): - if not os.path.exists(dst): - return dst - if not dst.endswith(APPENDIX): - dst += APPENDIX - if not os.path.exists(dst): - return dst - n = 0 - test_dst = dst + str(n) - while os.path.exists(test_dst): - n += 1 - test_dst = dst + str(n) - - return test_dst - -def copytree(src, dst, symlinks=False, ignore=None, overwrite=False): - """Recursively copy a directory tree using copy2(). - - The destination directory must not already exist. - If exception(s) occur, an Error is raised with a list of reasons. - - If the optional symlinks flag is true, symbolic links in the - source tree result in symbolic links in the destination tree; if - it is false, the contents of the files pointed to by symbolic - links are copied. - - The optional ignore argument is a callable. If given, it - is called with the `src` parameter, which is the directory - being visited by copytree(), and `names` which is the list of - `src` contents, as returned by os.listdir(): - - callable(src, names) -> ignored_names - - Since copytree() is called recursively, the callable will be - called once for each directory that is copied. It returns a - list of names relative to the `src` directory that should - not be copied. - - XXX Consider this example code rather than the ultimate tool. - - """ - names = os.listdir(src) - if ignore is not None: - ignored_names = ignore(src, names) - else: - ignored_names = set() - - errors = [] - try: - os.makedirs(dst) - except Exception as err: - if not overwrite: - dst = get_safe_path(dst) - os.makedirs(dst) - for name in names: - if name in ignored_names: - continue - srcname = os.path.join(src, name) - dstname = os.path.join(dst, name) - try: - if symlinks and os.path.islink(srcname): - linkto = os.readlink(srcname) - if os.path.lexists(dstname): - if not os.path.islink(dstname) \ - or os.readlink(dstname) != linkto: - os.unlink(dstname) - os.symlink(linkto, dstname) - elif os.path.isdir(srcname): - for _ in copytree(srcname, dstname, symlinks, - ignore, overwrite): - yield - else: - # Will raise a SpecialFileError for unsupported file types - for _ in copy2(srcname, dstname, - overwrite=overwrite, symlinks=symlinks): - yield - # catch the Error from the recursive copytree so that we can - # continue with other files - except Error as err: - errors.extend(err.args[0]) - except EnvironmentError as why: - errors.append((srcname, dstname, str(why))) - try: - copystat(src, dst) - except OSError as why: - if WindowsError is not None and isinstance(why, WindowsError): - # Copying file access times may fail on Windows - pass - else: - errors.extend((src, dst, str(why))) - if errors: - raise Error(errors) - -def rmtree(path, ignore_errors=False, onerror=None): - """Recursively delete a directory tree. - - If ignore_errors is set, errors are ignored; otherwise, if onerror - is set, it is called to handle the error with arguments (func, - path, exc_info) where func is os.listdir, os.remove, or os.rmdir; - path is the argument to that function that caused it to fail; and - exc_info is a tuple returned by sys.exc_info(). If ignore_errors - is false and onerror is None, an exception is raised. - - """ - if ignore_errors: - def onerror(*args): - pass - elif onerror is None: - def onerror(*args): - raise - try: - if os.path.islink(path): - # symlinks to directories are forbidden, see bug #1669 - raise OSError("Cannot call rmtree on a symbolic link") - except OSError: - onerror(os.path.islink, path, sys.exc_info()) - # can't continue even if onerror hook returns - return - names = [] - try: - names = os.listdir(path) - except os.error as err: - onerror(os.listdir, path, sys.exc_info()) - for name in names: - fullname = os.path.join(path, name) - try: - mode = os.lstat(fullname).st_mode - except os.error: - mode = 0 - if stat.S_ISDIR(mode): - rmtree(fullname, ignore_errors, onerror) - else: - try: - os.remove(fullname) - except os.error as err: - onerror(os.remove, fullname, sys.exc_info()) - try: - os.rmdir(path) - except os.error: - onerror(os.rmdir, path, sys.exc_info()) - - -def _basename(path): - # A basename() variant which first strips the trailing slash, if present. - # Thus we always get the last component of the path, even for directories. - return os.path.basename(path.rstrip(os.path.sep)) - -def move(src, dst, overwrite=False): - """Recursively move a file or directory to another location. This is - similar to the Unix "mv" command. - - If the destination is a directory or a symlink to a directory, the source - is moved inside the directory. The destination path must not already - exist. - - If the destination already exists but is not a directory, it may be - overwritten depending on os.rename() semantics. - - If the destination is on our current filesystem, then rename() is used. - Otherwise, src is copied to the destination and then removed. - A lot more could be done here... A look at a mv.c shows a lot of - the issues this implementation glosses over. - - """ - real_dst = os.path.join(dst, _basename(src)) - if not overwrite: - real_dst = get_safe_path(real_dst) - try: - os.rename(src, real_dst) - except OSError: - if os.path.isdir(src): - if _destinsrc(src, dst): - raise Error("Cannot move a directory '%s' into itself '%s'." % (src, dst)) - for _ in copytree(src, real_dst, symlinks=True, overwrite=overwrite): - yield - rmtree(src) - else: - for _ in copy2(src, real_dst, symlinks=True, overwrite=overwrite): - yield - os.unlink(src) - -def _destinsrc(src, dst): - src = abspath(src) - dst = abspath(dst) - if not src.endswith(os.path.sep): - src += os.path.sep - if not dst.endswith(os.path.sep): - dst += os.path.sep - return dst.startswith(src) - -# vi: expandtab sts=4 ts=4 sw=4 diff --git a/ranger/fsobject/directory.py b/ranger/fsobject/directory.py index e52b84d7..7578ad5f 100644 --- a/ranger/fsobject/directory.py +++ b/ranger/fsobject/directory.py @@ -21,6 +21,7 @@ from os.path import join, isdir, basename from collections import deque from time import time +from ranger.core.loader import Loadable from ranger.ext.mount_path import mount_path from ranger.fsobject import BAD_INFO, File, FileSystemObject from ranger.core.shared import SettingsAware @@ -57,7 +58,7 @@ def accept_file(fname, hidden_filter, name_filter): return False return True -class Directory(FileSystemObject, Accumulator, SettingsAware): +class Directory(FileSystemObject, Accumulator, Loadable, SettingsAware): is_directory = True enterable = False load_generator = None |