diff options
-rw-r--r-- | ranger/core/actions.py | 70 | ||||
-rw-r--r-- | ranger/ext/run_forked.py | 26 | ||||
-rw-r--r-- | ranger/ext/shutil_generatorized.py | 302 |
3 files changed, 364 insertions, 34 deletions
diff --git a/ranger/core/actions.py b/ranger/core/actions.py index 2f76527a..9d839d4e 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -22,7 +22,8 @@ from ranger.core.shared import FileManagerAware, EnvironmentAware, \ SettingsAware from ranger.core.tab import Tab from ranger.fsobject import File -from ranger.core.loader import CommandLoader +from ranger.core.loader import CommandLoader, Loadable +from ranger.ext import shutil_generatorized as shutil_g MACRO_FAIL = "<\x01\x01MACRO_HAS_NO_VALUE\x01\01>" @@ -1061,53 +1062,54 @@ class Actions(FileManagerAware, EnvironmentAware, SettingsAware): def paste(self, overwrite=False): """Paste the selected items into the current directory""" - copied_files = tuple(self.copy_buffer) + copied_files = tuple(self.env.copy) if not copied_files: return - def refresh(_): - cwd = self.get_directory(original_path) - cwd.load_content() - - cwd = self.thisdir - original_path = cwd.path - one_file = copied_files[0] - if overwrite: - cp_flags = ['-af', '--'] - mv_flags = ['-f', '--'] - else: - cp_flags = ['--backup=numbered', '-a', '--'] - mv_flags = ['--backup=numbered', '--'] + original_path = self.env.cwd.path + try: + one_file = copied_files[0] + except: + one_file = None - if self.do_cut: - self.copy_buffer.clear() - self.do_cut = False + if self.env.cut: + self.env.copy.clear() + self.env.cut = False if len(copied_files) == 1: descr = "moving: " + one_file.path else: descr = "moving files from: " + one_file.dirname - obj = CommandLoader(args=['mv'] + mv_flags \ - + [f.path for f in copied_files] \ - + [cwd.path], descr=descr) + 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() else: if len(copied_files) == 1: descr = "copying: " + one_file.path else: descr = "copying files from: " + one_file.dirname - if not overwrite and len(copied_files) == 1 \ - and one_file.dirname == cwd.path: - # Special case: yypp - # copying a file onto itself -> create a backup - obj = CommandLoader(args=['cp', '-f'] + cp_flags \ - + [one_file.path, one_file.path], descr=descr) - else: - obj = CommandLoader(args=['cp'] + cp_flags \ - + [f.path for f in copied_files] \ - + [cwd.path], descr=descr) - - obj.signal_bind('after', refresh) - self.loader.add(obj) + 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(Loadable(generate(), descr)) def delete(self): # XXX: warn when deleting mount points/unseen marked files? diff --git a/ranger/ext/run_forked.py b/ranger/ext/run_forked.py new file mode 100644 index 00000000..0dd52252 --- /dev/null +++ b/ranger/ext/run_forked.py @@ -0,0 +1,26 @@ +# Copyright (C) 2012 Roman Zimbelmann <romanz@lavabit.com> +# This software is distributed under the terms of the GNU GPL version 3. + +import os +import subprocess + +def Popen_forked(*args, **kwargs): + """ + Forks process and runs Popen with the given args and kwargs. + + If os.fork() is not supported, runs Popen without forking and returns the + process object returned by Popen. + Otherwise, returns None. + """ + try: + pid = os.fork() + except: + # fall back to not forking if os.fork() is not supported + return subprocess.Popen(*args, **kwargs) + else: + if pid == 0: + os.setsid() + kwargs['stdin'] = open(os.devnull, 'r') + kwargs['stdout'] = kwargs['stderr'] = open(os.devnull, 'w') + subprocess.Popen(*args, **kwargs) + os._exit(0) diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py new file mode 100644 index 00000000..436d2cb7 --- /dev/null +++ b/ranger/ext/shutil_generatorized.py @@ -0,0 +1,302 @@ +# 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 |