1
0
Fork 0
mirror of synced 2024-12-23 22:43:23 -05:00

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:
Petter Hansegård 2023-09-27 21:31:47 +02:00
parent 3f9e409669
commit 22a7cbb6f7
3 changed files with 105 additions and 13 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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."""