diff --git a/CHANGELOG.md b/CHANGELOG.md index 6467d77cb..3df3fc734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## WIP +- Add optional support to copy symlinks as links, rather than copying the target (via @jamesrtnz) - Added support for DBeaver (via @or-tal-0) ## Mackup 0.8.40 diff --git a/doc/README.md b/doc/README.md index d1aa0f600..c29ea6be2 100644 --- a/doc/README.md +++ b/doc/README.md @@ -81,6 +81,19 @@ path = some/path in your/home path = /some path/in/your/root ``` +### Copying Symlinks as Links + +By default, when Mackup backs up the application configuration, any symbolic +links to files results in the destination file being copied into the backup. + +You can optionally configure Mackup to ensure that any Symlinks are copied as +Links, and not files: + +```ini +[storage] +copy_symlinks = true +``` + ### Custom Directory Name You can customize the directory name in which Mackup stores your file. By diff --git a/mackup/config.py b/mackup/config.py index 9ad9a36d1..f9656211d 100644 --- a/mackup/config.py +++ b/mackup/config.py @@ -55,6 +55,9 @@ def __init__(self, filename=None): # Get the directory replacing 'Mackup', if any self._directory = self._parse_directory() + # Get the copy_symlinks value + self._copy_symlinks = self._parse_symlinks() + # Get the list of apps to ignore self._apps_to_ignore = self._parse_apps_to_ignore() @@ -129,6 +132,16 @@ def apps_to_sync(self): """ return set(self._apps_to_sync) + @property + def copy_symlinks(self): + """ + The copy_symlinks flag status. + + Returns: + bool + """ + return bool(self._copy_symlinks) + def _setup_parser(self, filename=None): """ Configure the ConfigParser instance the way we want it. @@ -221,6 +234,21 @@ def _parse_path(self): return str(path) + def _parse_symlinks(self): + """ + Parse the copy_symlinks setting in the config. + + Returns: + bool + """ + if self._parser.has_option("storage", "copy_symlinks"): + copy_symlinks = self._parser.getboolean("storage", "copy_symlinks", fallback=False) + else: + copy_symlinks = False + + return bool(copy_symlinks) + + def _parse_directory(self): """ Parse the storage directory in the config. diff --git a/mackup/main.py b/mackup/main.py index 198ac2563..2652aee18 100644 --- a/mackup/main.py +++ b/mackup/main.py @@ -78,6 +78,8 @@ def printAppHeader(app_name): if args["--root"]: utils.CAN_RUN_AS_ROOT = True + utils.COPY_SYMLINKS = mckp._config._copy_symlinks + dry_run = args["--dry-run"] verbose = args["--verbose"] diff --git a/mackup/utils.py b/mackup/utils.py index 6cf005f45..652ef7745 100644 --- a/mackup/utils.py +++ b/mackup/utils.py @@ -19,6 +19,8 @@ # Flag that control if mackup can be run as root CAN_RUN_AS_ROOT = False +# Are we going to copy symlinks as links? +COPY_SYMLINKS = False def confirm(question): """ @@ -98,18 +100,19 @@ def copy(src, dst): # We need to copy a single file if os.path.isfile(src): # Copy the src file to dst - shutil.copy(src, dst) + shutil.copy(src, dst, follow_symlinks=not(COPY_SYMLINKS)) # We need to copy a whole folder elif os.path.isdir(src): - shutil.copytree(src, dst) + shutil.copytree(src, dst, symlinks=COPY_SYMLINKS) # What the heck is this? else: raise ValueError("Unsupported file: {}".format(src)) # Set the good mode to the file or folder recursively - chmod(dst) + if COPY_SYMLINKS == False: + chmod(dst) def link(target, link_to): diff --git a/tests/config_tests.py b/tests/config_tests.py index 08109b3cc..a24debba0 100644 --- a/tests/config_tests.py +++ b/tests/config_tests.py @@ -35,6 +35,9 @@ def test_config_no_config(self): assert cfg.apps_to_ignore == set() assert cfg.apps_to_sync == set() + assert isinstance(cfg.copy_symlinks, bool) + assert cfg.copy_symlinks == False + def test_config_empty(self): cfg = Config("mackup-empty.cfg") @@ -53,6 +56,9 @@ def test_config_empty(self): assert cfg.apps_to_ignore == set() assert cfg.apps_to_sync == set() + assert isinstance(cfg.copy_symlinks, bool) + assert cfg.copy_symlinks == False + def test_config_engine_dropbox(self): cfg = Config("mackup-engine-dropbox.cfg") @@ -213,3 +219,9 @@ def test_config_apps_to_ignore_and_sync(self): def test_config_old_config(self): self.assertRaises(SystemExit, Config, "mackup-old-config.cfg") + + def test_config_copy_symlinks(self): + cfg = Config("mackup-copy-symlinks.cfg") + + assert isinstance(cfg.copy_symlinks, bool) + assert cfg.copy_symlinks == True diff --git a/tests/fixtures/mackup-copy-symlinks.cfg b/tests/fixtures/mackup-copy-symlinks.cfg new file mode 100644 index 000000000..8850e163b --- /dev/null +++ b/tests/fixtures/mackup-copy-symlinks.cfg @@ -0,0 +1,2 @@ +[storage] +copy_symlinks = 1