From 9a68a8660df28044f412817331cce6da5768eb5d Mon Sep 17 00:00:00 2001 From: Fuxino Date: Thu, 8 Oct 2026 20:29:21 +0200 Subject: [PATCH] Resolve the remote output path on the server The output path was made absolute with os.path.abspath and '~' was expanded to the local home, so relative paths and '~' pointed to the wrong folder on the server. Resolve the path on the server instead, where '~' and relative paths refer to the remote user's home, and document it. --- man/simple_backup.1 | 8 +++++ simple_backup/simple_backup.conf | 2 +- simple_backup/simple_backup.py | 58 +++++++++++++++++++++++--------- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/man/simple_backup.1 b/man/simple_backup.1 index b86e4ba..c3a9a46 100644 --- a/man/simple_backup.1 +++ b/man/simple_backup.1 @@ -146,6 +146,14 @@ Copy it to the default location ($HOME/.config/simple_backup) and edit it as nee It is possible to choose a directory on a remote server as destination for the backup. The files are copied by rsync through SSH. Server hostname and username must be specified, either in the configuration file, or on the command line (\(aq\-\-ssh\-host\(aq and \(aq\-\-ssh\-user\(aq options). +.P +The output directory is a path on the server. A leading \(aq~\(aq and relative paths refer to the +home directory of the user on the server. On the command line, quote \(aq~\(aq so that the local +shell doesn\(aqt expand it, for example: +.P +.EX + simple_backup \-\-ssh\-host server \-\-ssh\-user user \-o \(aq~/backups\(aq +.EE .SS AUTHENTICATION For authentication, it is possible to use SSH key or password. .P diff --git a/simple_backup/simple_backup.conf b/simple_backup/simple_backup.conf index d316dee..0cbdb66 100644 --- a/simple_backup/simple_backup.conf +++ b/simple_backup/simple_backup.conf @@ -4,7 +4,7 @@ # Files and directories to backup. Multiple items can be separated using a comma (','). It is possible to use wildcards (i.e. '*' to match multiple characters and '~' for the user's home directory). inputs=/home/user -# Output directory. +# Output directory. For remote backups, this is a path on the server ('~' and relative paths refer to the home directory on the server). backup_dir=/media/Backup # Files, directories and patterns to exclude from the backup. Multiple items can be separated using a comma. diff --git a/simple_backup/simple_backup.py b/simple_backup/simple_backup.py index 59a711f..ca810a4 100755 --- a/simple_backup/simple_backup.py +++ b/simple_backup/simple_backup.py @@ -189,28 +189,48 @@ class Backup: if self._ssh is None: return 5 - _, stdout, _ = self._ssh.exec_command( - f'if [ -d {shlex.quote(self.output)} ]; then echo "ok"; fi') + output = self._resolve_remote_path(self.output) - output = stdout.read().decode('utf-8').strip() - - if output != 'ok': + if output is None: logger.critical('Output path for backup does not exist') return 2 + + self.output = output else: if not os.path.isdir(self.output): logger.critical('Output path for backup does not exist') return 2 - self.output = os.path.abspath(self.output) + self.output = os.path.abspath(self.output) if self.keep is None: self.keep = -1 return 0 + def _resolve_remote_path(self, path: str) -> Optional[str]: + """Return the absolute path of a directory on the server, or None if it doesn't exist""" + + assert self._ssh is not None + + # Leave a leading '~' unquoted, so that the server's shell expands it to the remote home. + # Relative paths are resolved from the remote home, where the command starts + if path == '~' or path.startswith('~/'): + target = f'~/{shlex.quote(path[2:])}' + else: + target = shlex.quote(path) + + _, stdout, _ = self._ssh.exec_command(f'cd -- {target} && pwd') + + output = stdout.read().decode('utf-8').strip().split('\n')[-1] + + if not output.startswith('/'): + return None + + return output + # Function to create the actual backup directory def define_backup_dir(self) -> None: """Define the actual backup dir""" @@ -768,14 +788,8 @@ def _read_config(config_file, user: Optional[str] = None): config_args['inputs'] = inputs - output = config.get(section, 'backup_dir') - - if user is not None: - output = _expand_home(output, user) - elif user is None and output.startswith('~'): - logger.warning('Cannot expand \'~\', no user specified') - - config_args['output'] = output + # '~' is expanded later, since for remote backups it refers to the home on the server + config_args['output'] = config.get(section, 'backup_dir') try: exclude = config.get(section, 'exclude') @@ -928,11 +942,25 @@ def simple_backup() -> int: return 6 inputs = args.inputs if args.inputs is not None else config_args['inputs'] - output = args.output if args.output is not None else config_args['output'] exclude = args.exclude if args.exclude is not None else config_args['exclude'] keep = args.keep if args.keep is not None else config_args['keep'] ssh_host = args.ssh_host if args.ssh_host is not None else config_args['ssh_host'] ssh_user = args.ssh_user if args.ssh_user is not None else config_args['ssh_user'] + + if args.output is not None: + output = args.output + else: + output = config_args['output'] + + # For remote backups, '~' is expanded on the server + remote = ssh_host is not None and ssh_user is not None + + if output is not None and not remote: + if user is not None: + output = _expand_home(output, user) + elif output.startswith('~'): + logger.warning('Cannot expand \'~\', no user specified') + ssh_keyfile = args.keyfile if args.keyfile is not None else config_args['ssh_keyfile'] remote_sudo = args.remote_sudo or config_args['remote_sudo']