#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import ( absolute_import, division, print_function, unicode_literals ) from nine import ( IS_PYTHON2, basestring, chr, class_types, filter, integer_types, implements_iterator, implements_to_string, implements_repr, input, iterkeys, iteritems, itervalues, long, map, native_str, nine, nimport, range, range_list, reraise, str, zip ) argparse = nimport('argparse') contextlib = nimport('contextlib') glob = nimport('glob') grp = nimport('grp') os = nimport('os') pwd = nimport('pwd') subprocess = nimport('subprocess') sys = nimport('sys') time = nimport('time') DEFAULT_PREFIX = 'home_' DEFAULT_SUFFIX = '.tar.gz' DEFAULT_KEEP = 5 def run(): """Runs this program. The program generates a new tar file for a given home directory. """ start_time = time.time() options = parse_cmd_line( default_prefix=DEFAULT_PREFIX, default_suffix=DEFAULT_SUFFIX, default_keep=DEFAULT_KEEP) remove_old_files( pattern='{destdir}{sep}{prefix}*{suffix}'.format( destdir=options['destdir'], sep=os.sep, prefix=options['prefix'], suffix=options['suffix']), keep=options['keep'] - 1, # subtract 1 because new tar will be created remove=options['remove'], verbose=options['verbose']) dest_tar=generate_tar_name( dest_dir=options['destdir'], prefix=options['prefix'], suffix=options['suffix']) tar_src_dir( src_dir=options['srcdir'], dest_tar=dest_tar, verbose=options['verbose']) change_file_owner_group( filename=dest_tar, user=options['user'], group=options['user'], verbose=options['verbose']) if options['verbose']: print('Done, in {} seconds!'.format(time.time() - start_time)) def generate_tar_name(**kwargs): """Generates a (hopefully) unique fully qualified tar filename. Arguments: kwargs: a dictionary with the following keys:- dest_dir: where to save the tar file prefix: the prefix to use for the tar file suffix: the suffix to use for the tar file Returns: A unique tar name of the format: /second> where the year, month, day, etc. values represent the local time at the point at which this function is called. """ dest_dir = kwargs.pop('dest_dir') prefix = kwargs.pop('prefix') suffix = kwargs.pop('suffix') if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) localtime = time.localtime() return ('{dest_dir}{sep}{prefix}{year:04d}{month:02d}{day:02d}' + '{hour:02d}{minute:02d}{second:02d}{suffix}').format( dest_dir=dest_dir, prefix=prefix, suffix=suffix, year=localtime[0], month=localtime[1], day=localtime[2], hour=localtime[3], minute=localtime[4], second=localtime[5], sep=os.sep) def parse_cmd_line(**kwargs): """Parses the command-line arguments. Arguments: kwargs: a dictionary with the following keys:- default_prefix: the default prefix for generated tar files default_suffix: the default suffix for generated tar files default_keep: the default number of tar files to keep if removing old tar files Returns: A dictionary with each of the supplied command-line arguments. If no value for an item was supplied on the command-line, then the default value for that it is returned. """ default_prefix = kwargs.pop('default_prefix') default_suffix = kwargs.pop('default_suffix') default_keep = kwargs.pop('default_keep') if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) parser = argparse.ArgumentParser( description="Archives (tars) the contents of a user's home directory") parser.add_argument( 'user', help=' '.join([ 'specify the owner of the archive file;', 'the archive files user and group will be set to this value'])) parser.add_argument( 'srcdir', help='specify the owners home directory') parser.add_argument( 'destdir', help='specify the directory that the new archive file will be saved to') parser.add_argument( '--prefix', '-p', default=default_prefix, help=' '.join([ 'specify the prefix that the newly created archive file will have', "(default: '%(default)s')"])) parser.add_argument( '--suffix', '-x', default=default_suffix, help=' '.join([ 'specify the suffix that the newly created archive file will have', "(default: '%(default)s')"])) parser.add_argument( '--keep', '-k', type=int, default=default_keep, help='specify how many archives to keep (default: %(default)i)') parser.add_argument( '--remove', '-r', action='store_true', default=False, help='specify this so that obsolete archives will be removed') parser.add_argument( '--verbose', '-v', action='store_true', default=False, help='specify this to display verbose output') # vars() turns Namespace into a regular dictionary options = vars(parser.parse_args()) options['srcdir'] = chomp_sep(options['srcdir']) options['destdir'] = chomp_sep(options['destdir']) options['keep'] = max(options['keep'], 1) # keep must be at least 1 return options def chomp_sep(dir_name): """Removes any trailing directory separator characters from the given directory name. Arguments: dir_name: the name that has to have any trailing slashes removed Returns: The directory name with no trailing separator characters """ while dir_name.endswith(os.sep): dir_name = dir_name[:-1] return dir_name def remove_old_files(**kwargs): """Only keeps the newest files that match the given pattern. Arguments: kwargs: a dictionary with the following keys:- 'pattern' - the glob pattern of existing files 'keep' - the number of existing files to keep 'remove' - whether or not to remove old files 'verbose' - whether or not to output text describing non-fatal events Returns: None """ pattern = kwargs.pop('pattern') keep= kwargs.pop('keep') remove = kwargs.pop('remove') verbose= kwargs.pop('verbose') if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) if not remove: return matching = glob.glob(pattern) if keep >= len(matching): if verbose: print("No files matching pattern '{}' need to be removed".format( pattern)) return files = [tup[1] for tup in sorted([(os.path.getmtime(f), f) for f in matching])] for f in files[keep:]: os.remove(f) if verbose: print("Removed file '{filename}'".format(filename=f)) def tar_src_dir(src_dir, dest_tar, verbose): """Archives the source directory into a newly created destination tar. Arguments: src_dir: the source directory to be tarred dest_tar: the name of the tar file to create verbose: whether or not to output text describing non-fatal events Returns: None """ src_dir = os.path.abspath(src_dir) index = src_dir.rindex(os.sep) base_dir = src_dir[0:index] home_dir = src_dir[index+1:] if verbose: verbose_flag = '--verbose ' else: verbose_flag = '' tar_cmd = ''.join(['sudo tar ', '--create --preserve-permissions --atime-preserve ', "--use-compress-program='pigz' ", verbose_flag, "--file={} ".format(dest_tar), home_dir, ]) with chdir(base_dir): if verbose: subprocess.call(tar_cmd, shell=True) else: subprocess.call('{} {}'.format(tar_cmd, '&>/var/null'), shell=True) # Ensure that the file is flushed to disk subprocess.call('sync; sleep 10; sync', shell=True) @contextlib.contextmanager def chdir(new_dir): """Context manager which allows code to run in a different directory. (1) Stores the current working directory; (2) switches to the directory: ; (3) executes any code run in this context; and then (4) returns to the original working directory. Returns: None """ cwd = os.getcwd() os.chdir(new_dir) try: yield finally: os.chdir(cwd) def change_file_owner_group(**kwargs): """Changes the owner and group of the named file. Arguments: kwargs: a dictionary with the following keys:- 'filename': the file whose ownership is to be changed 'user': the new owner to assign to the file 'group': the new group to assign to the file; this is the same as 'user' if not given 'verbose': whether to output text describing non-fatal events Returns: None """ filename = kwargs.pop('filename') user = kwargs.pop('user') group = kwargs.pop('group', user) verbose= kwargs.pop('verbose') if kwargs: raise TypeError('Unexpected **kwargs: %r' % kwargs) try: os.chown(filename, pwd.getpwnam(user).pw_uid, grp.getgrnam(group).gr_gid) if verbose: print("Changed ownership of '{filename}' to {user}:{group}".format( filename=filename, user=user, group=group)) except os.OSError: print("Unable to change ownership of " + "'{filename}' to {user}:{group}".format( filename=filename, user=user, group=group), file=sys.stderr) if __name__ == '__main__': run()