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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user