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
|
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
|
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).
|
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
|
.SS AUTHENTICATION
|
||||||
For authentication, it is possible to use SSH key or password.
|
For authentication, it is possible to use SSH key or password.
|
||||||
.P
|
.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).
|
# 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
|
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
|
backup_dir=/media/Backup
|
||||||
|
|
||||||
# Files, directories and patterns to exclude from the backup. Multiple items can be separated using a comma.
|
# Files, directories and patterns to exclude from the backup. Multiple items can be separated using a comma.
|
||||||
|
|||||||
@@ -189,15 +189,14 @@ class Backup:
|
|||||||
if self._ssh is None:
|
if self._ssh is None:
|
||||||
return 5
|
return 5
|
||||||
|
|
||||||
_, stdout, _ = self._ssh.exec_command(
|
output = self._resolve_remote_path(self.output)
|
||||||
f'if [ -d {shlex.quote(self.output)} ]; then echo "ok"; fi')
|
|
||||||
|
|
||||||
output = stdout.read().decode('utf-8').strip()
|
if output is None:
|
||||||
|
|
||||||
if output != 'ok':
|
|
||||||
logger.critical('Output path for backup does not exist')
|
logger.critical('Output path for backup does not exist')
|
||||||
|
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
self.output = output
|
||||||
else:
|
else:
|
||||||
if not os.path.isdir(self.output):
|
if not os.path.isdir(self.output):
|
||||||
logger.critical('Output path for backup does not exist')
|
logger.critical('Output path for backup does not exist')
|
||||||
@@ -211,6 +210,27 @@ class Backup:
|
|||||||
|
|
||||||
return 0
|
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
|
# Function to create the actual backup directory
|
||||||
def define_backup_dir(self) -> None:
|
def define_backup_dir(self) -> None:
|
||||||
"""Define the actual backup dir"""
|
"""Define the actual backup dir"""
|
||||||
@@ -768,14 +788,8 @@ def _read_config(config_file, user: Optional[str] = None):
|
|||||||
|
|
||||||
config_args['inputs'] = inputs
|
config_args['inputs'] = inputs
|
||||||
|
|
||||||
output = config.get(section, 'backup_dir')
|
# '~' is expanded later, since for remote backups it refers to the home on the server
|
||||||
|
config_args['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
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exclude = config.get(section, 'exclude')
|
exclude = config.get(section, 'exclude')
|
||||||
@@ -928,11 +942,25 @@ def simple_backup() -> int:
|
|||||||
return 6
|
return 6
|
||||||
|
|
||||||
inputs = args.inputs if args.inputs is not None else config_args['inputs']
|
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']
|
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']
|
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_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']
|
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']
|
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']
|
remote_sudo = args.remote_sudo or config_args['remote_sudo']
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user