From 35b87c859e5886d2c0638e41fdb34135d2ed60e9 Mon Sep 17 00:00:00 2001 From: Fuxino Date: Tue, 20 Jun 2023 19:22:22 +0200 Subject: [PATCH] Add --user arg --- man/simple_backup.1 | 33 +++++++------ simple_backup/simple_backup.py | 85 +++++++++++++++++++++++----------- 2 files changed, 76 insertions(+), 42 deletions(-) diff --git a/man/simple_backup.1 b/man/simple_backup.1 index 77b49d8..ea49b56 100644 --- a/man/simple_backup.1 +++ b/man/simple_backup.1 @@ -39,7 +39,7 @@ Parameters specified on the command line will override those in the configuratio .SH OPTIONS .TP .B \-h, \-\-help -Print a short help message and exit +Print a short help message and exit. .TP .B \-c, \-\-config FILE Specify the configuration file, useful to specify a different one from the default. @@ -52,21 +52,24 @@ or to use single or double quotes around them. .B \-o, \-\-output DIR Specify the directory where the files will be copied. The program will automatically create a subdirectory called \(aqsimple_backup\(aq (if it does not already exist) and -inside this directory the actual backup directory (using the current date and time) +inside this directory the actual backup directory (using the current date and time). .TP .B \-e, \-\-exclude FILE|DIR|PATTERN [FILE|...]] Specify files, directories or patterns to exclude from the backup. Matching files and directories -will not be copied. Multiple elements can be specified, in the same way as for the \-\-input option +will not be copied. Multiple elements can be specified, in the same way as for the \-\-input option. .TP .B \-k, \-\-keep N Specify how many old backups (so excluding the current one) will be kept. The default behavior -is to keep them all (same as N=\-1) +is to keep them all (same as N=\-1). +.TP +.B \-u, \-\-user USERNAME +Explicitly specify the user running the backup (in case it is needed for home directory expansion). .TP .B \-\-ssh\-host HOSTNAME -Hostname of the server where to copy the files in case of remote backup through SSH +Hostname of the server where to copy the files in case of remote backup through SSH. .TP .B \-\-ssh\-user USERNAME -Username for connecting to the server in case of remote backup +Username for connecting to the server in case of remote backup. .TP .B \-\-keyfile FILE Location of the SSH key for server authentication. @@ -85,7 +88,7 @@ before performing the backup. Default behavior is to remove old backups after successfully completing the backup. .TP .B \-\-no\-syslog -Don't use systemd journal for logging +Don't use systemd journal for logging. .TP .B \-\-rsync\-options OPTIONS [OPTION...] By default, the following rsync options are used: @@ -116,7 +119,7 @@ Run rsync on the remote server with sudo. This is needed if you want to preserve .RE .TP .B \-\-numeric\-ids -Use rsync \-\-numeric\-ids option. This causes rsync to use numeric uid/gid instead of trying to map uid/gid names from the local machine to the server +Use rsync \-\-numeric\-ids option. This causes rsync to use numeric uid/gid instead of trying to map uid/gid names from the local machine to the server. .SH CONFIGURATION An example configuration file is provided at \(aq/usr/share/doc/simple_backup/simple_backup.conf\(aq. Copy it to the default location ($HOME/.config/simple_backup) and edit it as needed. @@ -159,25 +162,25 @@ for details. For this reason, use SSH key authentication if possible. .SH EXIT STATUS .TP .B 0 -The backup was completed without errors +The backup was completed without errors. .TP .B 1 -No valid inputs selected for backup +No valid inputs selected for backup. .TP .B 2 -Backup failed because output directory for storing the backup does not exist +Backup failed because output directory for storing the backup does not exist. .TP .B 3 -Permission denied to access the output directory +Permission denied to access the output directory. .TP .B 4 -rsync error (rsync returned a non-zero value) +rsync error (rsync returned a non-zero value). .TP .B 5 -SSH connection failed +SSH connection failed. .TP .B 6 -Bad configuration file +Bad configuration file. .SH SEE ALSO .BR rsync (1) .SH AUTHORS diff --git a/simple_backup/simple_backup.py b/simple_backup/simple_backup.py index 63c6e36..08bf53d 100755 --- a/simple_backup/simple_backup.py +++ b/simple_backup/simple_backup.py @@ -51,15 +51,6 @@ except ImportError: load_dotenv() -euid = os.geteuid() - -if euid == 0: - user = os.getenv('SUDO_USER') - homedir = os.path.expanduser(f'~{user}') -else: - user = os.getenv('USER') - homedir = os.getenv('HOME') - logging.getLogger().setLevel(logging.DEBUG) logger = logging.getLogger(os.path.basename(__file__)) c_handler = StreamHandler() @@ -166,7 +157,7 @@ class Backup: self._password_auth = False self._password = None - def check_params(self): + def check_params(self, homedir=''): """Check if parameters for the backup are valid""" if self.inputs is None or len(self.inputs) == 0: @@ -183,10 +174,10 @@ class Backup: self._remote = True if self._remote: - self._ssh = self._ssh_connect() + self._ssh = self._ssh_connect(homedir) if self._ssh is None: - sys.exit(5) + return 5 _, stdout, _ = self._ssh.exec_command(f'if [ -d "{self.output}" ]; then echo "ok"; fi') @@ -315,7 +306,7 @@ class Backup: except IndexError: logger.info('No previous backups available') - def _ssh_connect(self): + def _ssh_connect(self, homedir=''): try: ssh = paramiko.SSHClient() except NameError: @@ -475,6 +466,8 @@ class Backup: rsync = f'/usr/bin/rsync {self.options} --link-dest="{self._last_backup}" --exclude-from=' +\ f'{self._exclude_path} --files-from={self._inputs_path} / "{self._server}{self._output_dir}"' + euid = os.geteuid() + if euid == 0 and self.ssh_keyfile is not None: rsync = f'{rsync} -e \'ssh -i {self.ssh_keyfile} -o StrictHostKeyChecking=no\'' elif self._password_auth and which('sshpass'): @@ -556,6 +549,15 @@ class Backup: def _parse_arguments(): + euid = os.geteuid() + + if euid == 0: + user = os.getenv('SUDO_USER') + else: + user = os.getenv('USER') + + homedir = os.path.expanduser(f'~{user}') + parser = argparse.ArgumentParser(prog='simple_backup', description='Simple backup script written in Python that uses rsync to copy files', epilog='See simple_backup(1) manpage for full documentation', @@ -567,11 +569,11 @@ def _parse_arguments(): parser.add_argument('-o', '--output', help='Output directory for the backup') parser.add_argument('-e', '--exclude', nargs='+', help='Files/directories/patterns to exclude from the backup') parser.add_argument('-k', '--keep', type=int, help='Number of old backups to keep') + parser.add_argument('-u', '--user', help='Explicitly specify the user running the backup') + parser.add_argument('-s', '--checksum', action='store_true', help='Use checksum rsync option to compare files') parser.add_argument('--ssh-host', help='Server hostname (for remote backup)') parser.add_argument('--ssh-user', help='Username to connect to server (for remote backup)') parser.add_argument('--keyfile', help='SSH key location') - parser.add_argument('-s', '--checksum', action='store_true', - help='Use checksum rsync option to compare files') parser.add_argument('-z', '--compress', action='store_true', help='Compress data during the transfer') parser.add_argument('--remove-before-backup', action='store_true', help='Remove old backups before executing the backup, instead of after') @@ -588,14 +590,20 @@ def _parse_arguments(): return args -def _expand_inputs(inputs): +def _expand_inputs(inputs, user=None): expanded_inputs = [] for i in inputs: if i == '': continue - i_ex = glob(os.path.expanduser(i.replace('~', f'~{user}'))) + if user is not None: + i_ex = glob(os.path.expanduser(i.replace('~', f'~{user}'))) + else: + i_ex = glob(i) + + if '~' in i: + logger.warning('Cannot expand \'~\'. No user specified') if len(i_ex) == 0: logger.warning('No file or directory matching input %s. Skipping...', i) @@ -605,7 +613,7 @@ def _expand_inputs(inputs): return expanded_inputs -def _read_config(config_file): +def _read_config(config_file, user=None): config_args = {'inputs': None, 'output': None, 'exclude': None, @@ -634,13 +642,17 @@ def _read_config(config_file): inputs = config.get(section, 'inputs') inputs = inputs.split(',') - inputs = _expand_inputs(inputs) + inputs = _expand_inputs(inputs, user) inputs = list(set(inputs)) config_args['inputs'] = inputs output = config.get(section, 'backup_dir') - output = os.path.expanduser(output.replace('~', f'~{user}')) + + if user is not None: + output = os.path.expanduser(output.replace('~', f'~{user}')) + elif user is None and '~' in output: + logger.warning('Cannot expand \'~\', no user specified') config_args['output'] = output @@ -694,12 +706,15 @@ def _read_config(config_file): def _notify(text): - _euid = os.geteuid() + euid = os.geteuid() - if _euid == 0: + if euid == 0: uid = os.getenv('SUDO_UID') else: - uid = os.geteuid() + uid = euid + + if uid is None: + return os.seteuid(int(uid)) os.environ['DBUS_SESSION_BUS_ADDRESS'] = f'unix:path=/run/user/{uid}/bus' @@ -708,7 +723,7 @@ def _notify(text): obj = dbus.Interface(obj, 'org.freedesktop.Notifications') obj.Notify('', 0, '', 'simple_backup', text, [], {'urgency': 1}, 10000) - os.seteuid(int(_euid)) + os.seteuid(int(euid)) def simple_backup(): @@ -716,6 +731,22 @@ def simple_backup(): args = _parse_arguments() + if args.user: + user = args.user + homedir = os.path.expanduser(f'~{user}') + else: + euid = os.geteuid() + + if euid == 0: + user = os.getenv('SUDO_USER') + homedir = os.path.expanduser(f'~{user}') + else: + user = os.getenv('USER') + homedir = os.getenv('HOME') + + if homedir is None: + homedir = '' + if args.no_syslog: try: logger.removeHandler(j_handler) @@ -723,10 +754,10 @@ def simple_backup(): pass try: - config_args = _read_config(args.config) + config_args = _read_config(args.config, user) except (configparser.NoSectionError, configparser.NoOptionError): logger.critical('Bad configuration file') - sys.exit(6) + 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'] @@ -759,7 +790,7 @@ def simple_backup(): backup = Backup(inputs, output, exclude, keep, rsync_options, ssh_host, ssh_user, ssh_keyfile, remote_sudo, remove_before=args.remove_before_backup) - return_code = backup.check_params() + return_code = backup.check_params(homedir) if return_code == 0: return backup.run()