1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2024-11-16 12:19:03 +00:00
dupeguru/hscommon/conflict.py

85 lines
2.9 KiB
Python
Raw Normal View History

2019-09-10 00:54:28 +00:00
# Created By: Virgil Dupras
# Created On: 2008-01-08
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
2019-09-10 00:54:28 +00:00
# http://www.gnu.org/licenses/gpl-3.0.html
"""When you have to deal with names that have to be unique and can conflict together, you can use
this module that deals with conflicts by prepending unique numbers in ``[]`` brackets to the name.
"""
import re
import os
import shutil
from pathlib import Path
2022-05-10 04:36:39 +00:00
from typing import Callable, List
2019-09-10 00:54:28 +00:00
# This matches [123], but not [12] (3 digits being the minimum).
# It also matches [1234] [12345] etc..
# And only at the start of the string
re_conflict = re.compile(r"^\[\d{3}\d*\] ")
2019-09-10 00:54:28 +00:00
2022-05-10 04:36:39 +00:00
def get_conflicted_name(other_names: List[str], name: str) -> str:
2019-09-10 00:54:28 +00:00
"""Returns name with a ``[000]`` number in front of it.
2019-09-10 00:54:28 +00:00
The number between brackets depends on how many conlicted filenames
there already are in other_names.
"""
name = get_unconflicted_name(name)
if name not in other_names:
return name
i = 0
while True:
newname = "[%03d] %s" % (i, name)
2019-09-10 00:54:28 +00:00
if newname not in other_names:
return newname
i += 1
2022-05-10 04:36:39 +00:00
def get_unconflicted_name(name: str) -> str:
2019-09-10 00:54:28 +00:00
"""Returns ``name`` without ``[]`` brackets.
2019-09-10 00:54:28 +00:00
Brackets which, of course, might have been added by func:`get_conflicted_name`.
"""
return re_conflict.sub("", name, 1)
2019-09-10 00:54:28 +00:00
2022-05-10 04:36:39 +00:00
def is_conflicted(name: str) -> bool:
"""Returns whether ``name`` is prepended with a bracketed number."""
2019-09-10 00:54:28 +00:00
return re_conflict.match(name) is not None
2022-05-10 04:36:39 +00:00
def _smart_move_or_copy(operation: Callable, source_path: Path, dest_path: Path) -> None:
"""Use move() or copy() to move and copy file with the conflict management."""
if dest_path.is_dir() and not source_path.is_dir():
dest_path = dest_path.joinpath(source_path.name)
2019-09-10 00:54:28 +00:00
if dest_path.exists():
filename = dest_path.name
dest_dir_path = dest_path.parent
2019-09-10 00:54:28 +00:00
newname = get_conflicted_name(os.listdir(str(dest_dir_path)), filename)
dest_path = dest_dir_path.joinpath(newname)
2019-09-10 00:54:28 +00:00
operation(str(source_path), str(dest_path))
2022-05-10 04:36:39 +00:00
def smart_move(source_path: Path, dest_path: Path) -> None:
"""Same as :func:`smart_copy`, but it moves files instead."""
2019-09-10 00:54:28 +00:00
_smart_move_or_copy(shutil.move, source_path, dest_path)
2022-05-10 04:36:39 +00:00
def smart_copy(source_path: Path, dest_path: Path) -> None:
"""Copies ``source_path`` to ``dest_path``, recursively and with conflict resolution."""
2019-09-10 00:54:28 +00:00
try:
_smart_move_or_copy(shutil.copy, source_path, dest_path)
2022-04-28 01:53:12 +00:00
except OSError as e:
if e.errno in {
21,
13,
}: # it's a directory, code is 21 on OS X / Linux and 13 on Windows
2019-09-10 00:54:28 +00:00
_smart_move_or_copy(shutil.copytree, source_path, dest_path)
else:
raise