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.
This commit is contained in:
Fuxino
2026-10-08 20:29:21 +02:00
parent 73de4f8d25
commit 9a68a8660d
3 changed files with 52 additions and 16 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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.
+42 -14
View File
@@ -189,15 +189,14 @@ 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')
@@ -211,6 +210,27 @@ class Backup:
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']