1
0
Fork 0
mirror of synced 2024-11-22 08:15:34 -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

@ -176,11 +176,12 @@ configuration, instead of specifying source locations directly, targets are
mapped to extended configuration dictionaries. mapped to extended configuration dictionaries.
| Parameter | Explanation | | Parameter | Explanation |
| --- | --- | |------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| `path` | The source for the symlink, the same as in the shortcut syntax (default: null, automatic (see below)) | | `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) | | `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) | | `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) | | `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) | | `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) | | `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. | | `if` | Execute this in your `$SHELL` and only link if it is successful. |

View file

@ -34,6 +34,7 @@ class Link(Plugin):
relink = defaults.get("relink", False) relink = defaults.get("relink", False)
create = defaults.get("create", False) create = defaults.get("create", False)
use_glob = defaults.get("glob", False) use_glob = defaults.get("glob", False)
backup = defaults.get("backup", False)
base_prefix = defaults.get("prefix", "") base_prefix = defaults.get("prefix", "")
test = defaults.get("if", None) test = defaults.get("if", None)
ignore_missing = defaults.get("ignore-missing", False) ignore_missing = defaults.get("ignore-missing", False)
@ -49,6 +50,7 @@ class Link(Plugin):
relink = source.get("relink", relink) relink = source.get("relink", relink)
create = source.get("create", create) create = source.get("create", create)
use_glob = source.get("glob", use_glob) use_glob = source.get("glob", use_glob)
backup = source.get("backup", backup)
base_prefix = source.get("prefix", base_prefix) base_prefix = source.get("prefix", base_prefix)
ignore_missing = source.get("ignore-missing", ignore_missing) ignore_missing = source.get("ignore-missing", ignore_missing)
exclude_paths = source.get("exclude", exclude_paths) exclude_paths = source.get("exclude", exclude_paths)
@ -85,6 +87,8 @@ class Link(Plugin):
canonical_path, canonical_path,
force, force,
) )
if backup:
success &= self._backup(glob_link_destination)
success &= self._link( success &= self._link(
glob_full_item, glob_full_item,
glob_link_destination, glob_link_destination,
@ -107,6 +111,8 @@ class Link(Plugin):
continue continue
if force or relink: if force or relink:
success &= self._delete(path, destination, relative, canonical_path, force) 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) success &= self._link(path, destination, relative, canonical_path, ignore_missing)
if success: if success:
self._log.info("All links have been set up") self._log.info("All links have been set up")
@ -197,6 +203,21 @@ class Link(Plugin):
self._log.lowinfo("Creating directory %s" % parent) self._log.lowinfo("Creating directory %s" % parent)
return success 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): def _delete(self, source, path, relative, canonical_path, force):
success = True success = True
source = os.path.join(self._context.base_directory(canonical_path=canonical_path), source) 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")) 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): def test_link_glob_1(home, dotfiles, run_dotbot):
"""Verify globbing works.""" """Verify globbing works."""