Add backup option to link action
Added backup option (defaults to false) which will cause dotbot to backup already existing files by renaming them with suffix .dotbot-backup. Added unit tests and updated README.
This commit is contained in:
parent
3f9e409669
commit
22a7cbb6f7
3 changed files with 105 additions and 13 deletions
27
README.md
27
README.md
|
@ -175,19 +175,20 @@ Link commands support an optional extended configuration. In this type of
|
|||
configuration, instead of specifying source locations directly, targets are
|
||||
mapped to extended configuration dictionaries.
|
||||
|
||||
| Parameter | Explanation |
|
||||
| --- | --- |
|
||||
| `path` | The source for the symlink, the same as in the shortcut syntax (default: null, automatic (see below)) |
|
||||
| `create` | When true, create parent directories to the link as needed. (default: false) |
|
||||
| `relink` | Removes the old target if it's a symlink (default: false) |
|
||||
| `force` | Force removes the old target, file or folder, and forces a new link (default: false) |
|
||||
| `relative` | Use a relative path to the source when creating the symlink (default: false, absolute links) |
|
||||
| `canonicalize` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default: true, real paths) |
|
||||
| `if` | Execute this in your `$SHELL` and only link if it is successful. |
|
||||
| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default: false) |
|
||||
| `glob` | Treat `path` as a glob pattern, expanding patterns referenced below, linking all *files* matched. (default: false) |
|
||||
| `exclude` | Array of glob patterns to remove from glob matches. Uses same syntax as `path`. Ignored if `glob` is `false`. (default: empty, keep all matches) |
|
||||
| `prefix` | Prepend prefix prefix to basename of each file when linked, when `glob` is `true`. (default: '') |
|
||||
| Parameter | Explanation |
|
||||
|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `path` | The source for the symlink, the same as in the shortcut syntax (default: null, automatic (see below)) |
|
||||
| `create` | When true, create parent directories to the link as needed. (default: false) |
|
||||
| `relink` | Removes the old target if it's a symlink (default: false) |
|
||||
| `force` | Force removes the old target, file or folder, and forces a new link (default: false) |
|
||||
| `backup` | Backup existing files/directories if they exist, creating a backup with suffix `.dotbot-backup (default: false) |
|
||||
| `relative` | Use a relative path to the source when creating the symlink (default: false, absolute links) |
|
||||
| `canonicalize` | Resolve any symbolic links encountered in the source to symlink to the canonical path (default: true, real paths) |
|
||||
| `if` | Execute this in your `$SHELL` and only link if it is successful. |
|
||||
| `ignore-missing` | Do not fail if the source is missing and create the link anyway (default: false) |
|
||||
| `glob` | Treat `path` as a glob pattern, expanding patterns referenced below, linking all *files* matched. (default: false) |
|
||||
| `exclude` | Array of glob patterns to remove from glob matches. Uses same syntax as `path`. Ignored if `glob` is `false`. (default: empty, keep all matches) |
|
||||
| `prefix` | Prepend prefix prefix to basename of each file when linked, when `glob` is `true`. (default: '') |
|
||||
|
||||
When `glob: True`, Dotbot uses [glob.glob](https://docs.python.org/3/library/glob.html#glob.glob) to resolve glob paths, expanding Unix shell-style wildcards, which are **not** the same as regular expressions; Only the following are expanded:
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ class Link(Plugin):
|
|||
relink = defaults.get("relink", False)
|
||||
create = defaults.get("create", False)
|
||||
use_glob = defaults.get("glob", False)
|
||||
backup = defaults.get("backup", False)
|
||||
base_prefix = defaults.get("prefix", "")
|
||||
test = defaults.get("if", None)
|
||||
ignore_missing = defaults.get("ignore-missing", False)
|
||||
|
@ -49,6 +50,7 @@ class Link(Plugin):
|
|||
relink = source.get("relink", relink)
|
||||
create = source.get("create", create)
|
||||
use_glob = source.get("glob", use_glob)
|
||||
backup = source.get("backup", backup)
|
||||
base_prefix = source.get("prefix", base_prefix)
|
||||
ignore_missing = source.get("ignore-missing", ignore_missing)
|
||||
exclude_paths = source.get("exclude", exclude_paths)
|
||||
|
@ -85,6 +87,8 @@ class Link(Plugin):
|
|||
canonical_path,
|
||||
force,
|
||||
)
|
||||
if backup:
|
||||
success &= self._backup(glob_link_destination)
|
||||
success &= self._link(
|
||||
glob_full_item,
|
||||
glob_link_destination,
|
||||
|
@ -107,6 +111,8 @@ class Link(Plugin):
|
|||
continue
|
||||
if force or relink:
|
||||
success &= self._delete(path, destination, relative, canonical_path, force)
|
||||
if backup:
|
||||
success &= self._backup(destination)
|
||||
success &= self._link(path, destination, relative, canonical_path, ignore_missing)
|
||||
if success:
|
||||
self._log.info("All links have been set up")
|
||||
|
@ -197,6 +203,21 @@ class Link(Plugin):
|
|||
self._log.lowinfo("Creating directory %s" % parent)
|
||||
return success
|
||||
|
||||
def _backup(self, path):
|
||||
success = True
|
||||
if self._exists(path) and not self._is_link(path):
|
||||
file_to_backup = os.path.join(os.path.expanduser(path))
|
||||
backup_path = file_to_backup + ".dotbot-backup"
|
||||
self._log.debug(f"Try to backup file {file_to_backup} to {backup_path}")
|
||||
try:
|
||||
os.rename(file_to_backup, backup_path)
|
||||
except OSError:
|
||||
self._log.warning(f"Failed to backup file {file_to_backup} to {backup_path}")
|
||||
success = False
|
||||
else:
|
||||
self._log.lowinfo(f"Backed up file {file_to_backup} to {backup_path}")
|
||||
return success
|
||||
|
||||
def _delete(self, source, path, relative, canonical_path, force):
|
||||
success = True
|
||||
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source)
|
||||
|
|
|
@ -175,6 +175,76 @@ def test_link_force_overwrite_symlink(home, dotfiles, run_dotbot):
|
|||
assert os.path.isfile(os.path.join(home, ".dir", "f"))
|
||||
|
||||
|
||||
def test_backup_is_created_if_destination_exists(home, dotfiles, run_dotbot):
|
||||
"""Verify that the backup directory is created if destination exists."""
|
||||
|
||||
os.mkdir(os.path.join(home, ".dir"))
|
||||
dotfiles.write("dir")
|
||||
|
||||
config = [{"link": {"~/.dir": {"path": "dir", "backup": True}}}]
|
||||
dotfiles.write_config(config)
|
||||
run_dotbot()
|
||||
|
||||
assert os.path.exists(os.path.join(home, ".dir.dotbot-backup"))
|
||||
|
||||
|
||||
def test_backup_file_is_created_if_destination_exists(home, dotfiles, run_dotbot):
|
||||
"""Verify that a backup file is created if destination exists."""
|
||||
|
||||
open(os.path.join(home, ".file"), "a").close()
|
||||
dotfiles.write("file")
|
||||
|
||||
config = [{"link": {"~/.file": {"path": "file", "backup": True}}}]
|
||||
dotfiles.write_config(config)
|
||||
run_dotbot()
|
||||
|
||||
assert os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||
|
||||
|
||||
def test_backup_file_not_created_if_link(home, dotfiles, run_dotbot):
|
||||
"""Verify that a backup file isn't created if destination is a symlink."""
|
||||
|
||||
open(os.path.join(home, "file"), "a").close()
|
||||
dotfiles.write("file")
|
||||
os.symlink(os.path.join(home, "file"), os.path.join(home, ".file"))
|
||||
|
||||
config = [{"link": {"~/.file": {"path": "file", "backup": True}}}]
|
||||
dotfiles.write_config(config)
|
||||
with pytest.raises(SystemExit):
|
||||
run_dotbot()
|
||||
|
||||
assert not os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||
|
||||
|
||||
def test_backup_file_not_created_if_force(home, dotfiles, run_dotbot):
|
||||
"""Verify that a backup file is created while using force option."""
|
||||
|
||||
open(os.path.join(home, ".file"), "a").close()
|
||||
dotfiles.write("file")
|
||||
|
||||
config = [{"link": {"~/.file": {"path": "file", "backup": True, "force": True}}}]
|
||||
dotfiles.write_config(config)
|
||||
run_dotbot()
|
||||
|
||||
assert not os.path.exists(os.path.join(home, ".file.dotbot-backup"))
|
||||
|
||||
|
||||
def test_backup_error_if_dest_already_exists(home, dotfiles, run_dotbot):
|
||||
"""Verify an error is thrown if the backup already exists."""
|
||||
|
||||
os.mkdir(os.path.join(home, ".dir"))
|
||||
os.mkdir(os.path.join(home, ".dir.dotbot-backup"))
|
||||
open(os.path.join(home, ".dir.dotbot-backup", "f"), "a").close()
|
||||
dotfiles.write("dir")
|
||||
|
||||
config = [{"link": {"~/.dir": {"path": "dir", "backup": True}}}]
|
||||
dotfiles.write_config(config)
|
||||
with pytest.raises(SystemExit):
|
||||
run_dotbot()
|
||||
|
||||
assert os.path.exists(os.path.join(home, ".dir.dotbot-backup", "f"))
|
||||
|
||||
|
||||
def test_link_glob_1(home, dotfiles, run_dotbot):
|
||||
"""Verify globbing works."""
|
||||
|
||||
|
|
Loading…
Reference in a new issue