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
+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.
+43 -15
View File
@@ -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']