diff options
-rw-r--r-- | doc/ranger.1 | 6 | ||||
-rw-r--r-- | doc/ranger.pod | 4 | ||||
-rwxr-xr-x | ranger/config/commands.py | 31 | ||||
-rw-r--r-- | ranger/core/actions.py | 5 | ||||
-rw-r--r-- | ranger/core/loader.py | 12 | ||||
-rw-r--r-- | ranger/ext/safe_path.py | 22 | ||||
-rw-r--r-- | ranger/ext/shutil_generatorized.py | 42 |
7 files changed, 89 insertions, 33 deletions
diff --git a/doc/ranger.1 b/doc/ranger.1 index b9c46d54..6b23bbfb 100644 --- a/doc/ranger.1 +++ b/doc/ranger.1 @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "RANGER 1" -.TH RANGER 1 "ranger-1.9.2" "2019-12-21" "ranger manual" +.TH RANGER 1 "ranger-1.9.2" "2019-12-22" "ranger manual" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l @@ -677,6 +677,10 @@ also \*(L"da\*(R", \*(L"dr\*(R" and \*(L"dt\*(R" shortcuts equivalent to \*(L"ya .IX Item "pp" Paste the files which were previously copied or cut, like pressing Ctrl+V in modern \s-1GUI\s0 programs. +.Sp +Conflicts will be renamed by appending an '_' (and a counter if necessary), +resulting in \f(CW\*(C`file.ext_\*(C'\fR, \f(CW\*(C`file.ext_0\*(C'\fR, etc. If you prefer \f(CW\*(C`file_.ext\*(C'\fR you +can use the \f(CW\*(C`paste_ext\*(C'\fR command. .IP "po" 14 .IX Item "po" Paste the copied/cut files, overwriting existing files. diff --git a/doc/ranger.pod b/doc/ranger.pod index a955efd8..4f53ae35 100644 --- a/doc/ranger.pod +++ b/doc/ranger.pod @@ -638,6 +638,10 @@ also "da", "dr" and "dt" shortcuts equivalent to "ya", "yr" and "yt".) Paste the files which were previously copied or cut, like pressing Ctrl+V in modern GUI programs. +Conflicts will be renamed by appending an '_' (and a counter if necessary), +resulting in C<file.ext_>, C<file.ext_0>, etc. If you prefer C<file_.ext> you +can use the C<paste_ext> command. + =item po Paste the copied/cut files, overwriting existing files. diff --git a/ranger/config/commands.py b/ranger/config/commands.py index cb6fa132..06d5abb7 100755 --- a/ranger/config/commands.py +++ b/ranger/config/commands.py @@ -1959,3 +1959,34 @@ class yank(Command): in sorted(self.modes.keys()) if mode ) + + +class paste_ext(Command): + """ + :paste_ext + + Like paste but tries to rename conflicting files so that the + file extension stays intact (e.g. file_.ext). + """ + + @staticmethod + def make_safe_path(dst): + if not os.path.exists(dst): + return dst + + dst_name, dst_ext = os.path.splitext(dst) + + if not dst_name.endswith("_"): + dst_name += "_" + if not os.path.exists(dst_name + dst_ext): + return dst_name + dst_ext + n = 0 + test_dst = dst_name + str(n) + while os.path.exists(test_dst + dst_ext): + n += 1 + test_dst = dst_name + str(n) + + return test_dst + dst_ext + + def execute(self): + return self.fm.paste(make_safe_path=paste_ext.make_safe_path) diff --git a/ranger/core/actions.py b/ranger/core/actions.py index cd6c6afe..c3d7de86 100644 --- a/ranger/core/actions.py +++ b/ranger/core/actions.py @@ -24,6 +24,7 @@ import ranger from ranger.ext.direction import Direction from ranger.ext.relative_symlink import relative_symlink from ranger.ext.keybinding_parser import key_to_string, construct_keybinding +from ranger.ext.safe_path import get_safe_path from ranger.ext.shell_escape import shell_quote from ranger.ext.next_available_filename import next_available_filename from ranger.ext.rifle import squash_flags, ASK_COMMAND @@ -1587,7 +1588,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m link(source_path, next_available_filename(target_path)) - def paste(self, overwrite=False, append=False, dest=None): + def paste(self, overwrite=False, append=False, dest=None, make_safe_path=get_safe_path): """:paste Paste the selected items into the current directory or to dest @@ -1597,7 +1598,7 @@ class Actions( # pylint: disable=too-many-instance-attributes,too-many-public-m dest = self.thistab.path if isdir(dest): loadable = CopyLoader(self.copy_buffer, self.do_cut, overwrite, - dest) + dest, make_safe_path) self.loader.add(loadable, append=append) self.do_cut = False else: diff --git a/ranger/core/loader.py b/ranger/core/loader.py index 26b729b6..7d4b21e8 100644 --- a/ranger/core/loader.py +++ b/ranger/core/loader.py @@ -19,6 +19,7 @@ except ImportError: HAVE_CHARDET = False from ranger.core.shared import FileManagerAware +from ranger.ext.safe_path import get_safe_path from ranger.ext.signals import SignalDispatcher from ranger.ext.human_readable import human_readable @@ -51,12 +52,14 @@ class Loadable(object): class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instance-attributes progressbar_supported = True - def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None): + def __init__(self, copy_buffer, do_cut=False, overwrite=False, dest=None, + make_safe_path=get_safe_path): self.copy_buffer = tuple(copy_buffer) self.do_cut = do_cut self.original_copy_buffer = copy_buffer self.original_path = dest if dest is not None else self.fm.thistab.path self.overwrite = overwrite + self.make_safe_path = make_safe_path self.percent = 0 if self.copy_buffer: self.one_file = self.copy_buffer[0] @@ -108,7 +111,8 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan self.fm.tags.dump() n = 0 for n in shutil_g.move(src=fobj.path, dst=self.original_path, - overwrite=self.overwrite): + overwrite=self.overwrite, + make_safe_path=self.make_safe_path): self.percent = ((done + n) / size) * 100. yield done += n @@ -125,6 +129,7 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan dst=os.path.join(self.original_path, fobj.basename), symlinks=True, overwrite=self.overwrite, + make_safe_path=self.make_safe_path, ): self.percent = ((done + n) / size) * 100. yield @@ -132,7 +137,8 @@ class CopyLoader(Loadable, FileManagerAware): # pylint: disable=too-many-instan else: n = 0 for n in shutil_g.copy2(fobj.path, self.original_path, - symlinks=True, overwrite=self.overwrite): + symlinks=True, overwrite=self.overwrite, + make_safe_path=self.make_safe_path): self.percent = ((done + n) / size) * 100. yield done += n diff --git a/ranger/ext/safe_path.py b/ranger/ext/safe_path.py new file mode 100644 index 00000000..b172b577 --- /dev/null +++ b/ranger/ext/safe_path.py @@ -0,0 +1,22 @@ +# This file is part of ranger, the console file manager. +# License: GNU GPL version 3, see the file "AUTHORS" for details. + +import os + +SUFFIX = '_' + + +def get_safe_path(dst): + if not os.path.exists(dst): + return dst + if not dst.endswith(SUFFIX): + dst += SUFFIX + 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 diff --git a/ranger/ext/shutil_generatorized.py b/ranger/ext/shutil_generatorized.py index fe3cf068..a97a4d2d 100644 --- a/ranger/ext/shutil_generatorized.py +++ b/ranger/ext/shutil_generatorized.py @@ -7,11 +7,11 @@ import os import stat import sys from shutil import (_samefile, rmtree, _basename, _destinsrc, Error, SpecialFileError) +from ranger.ext.safe_path import get_safe_path __all__ = ["copyfileobj", "copyfile", "copystat", "copy2", "BLOCK_SIZE", "copytree", "move", "rmtree", "Error", "SpecialFileError"] -APPENDIX = '_' BLOCK_SIZE = 16 * 1024 @@ -103,22 +103,6 @@ else: pass -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 copyfileobj(fsrc, fdst, length=BLOCK_SIZE): """copy data from file-like object fsrc to file-like object fdst""" done = 0 @@ -153,7 +137,7 @@ def copyfile(src, dst): yield done -def copy2(src, dst, overwrite=False, symlinks=False): +def copy2(src, dst, overwrite=False, symlinks=False, make_safe_path=get_safe_path): """Copy data and all stat info ("cp -p src dst"). The destination may be a directory. @@ -162,7 +146,7 @@ def copy2(src, dst, overwrite=False, symlinks=False): if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) if not overwrite: - dst = get_safe_path(dst) + dst = make_safe_path(dst) if symlinks and os.path.islink(src): linkto = os.readlink(src) if overwrite and os.path.lexists(dst): @@ -175,7 +159,7 @@ def copy2(src, dst, overwrite=False, symlinks=False): def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches - symlinks=False, ignore=None, overwrite=False): + symlinks=False, ignore=None, overwrite=False, make_safe_path=get_safe_path): """Recursively copy a directory tree using copy2(). The destination directory must not already exist. @@ -211,7 +195,7 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches os.makedirs(dst) except OSError: if not overwrite: - dst = get_safe_path(dst) + dst = make_safe_path(dst) os.makedirs(dst) errors = [] done = 0 @@ -229,13 +213,15 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches copystat(srcname, dstname) elif os.path.isdir(srcname): n = 0 - for n in copytree(srcname, dstname, symlinks, ignore, overwrite): + for n in copytree(srcname, dstname, symlinks, ignore, overwrite, + make_safe_path): yield done + n done += n else: # Will raise a SpecialFileError for unsupported file types n = 0 - for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks): + for n in copy2(srcname, dstname, overwrite=overwrite, symlinks=symlinks, + make_safe_path=make_safe_path): yield done + n done += n # catch the Error from the recursive copytree so that we can @@ -256,7 +242,7 @@ def copytree(src, dst, # pylint: disable=too-many-locals,too-many-branches raise Error(errors) -def move(src, dst, overwrite=False): +def move(src, dst, overwrite=False, make_safe_path=get_safe_path): """Recursively move a file or directory to another location. This is similar to the Unix "mv" command. @@ -283,17 +269,19 @@ def move(src, dst, overwrite=False): real_dst = os.path.join(dst, _basename(src)) if not overwrite: - real_dst = get_safe_path(real_dst) + real_dst = make_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 done in copytree(src, real_dst, symlinks=True, overwrite=overwrite): + for done in copytree(src, real_dst, symlinks=True, overwrite=overwrite, + make_safe_path=make_safe_path): yield done rmtree(src) else: - for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite): + for done in copy2(src, real_dst, symlinks=True, overwrite=overwrite, + make_safe_path=make_safe_path): yield done os.unlink(src) |