Advertisement
justin_hanekom

tar-home.py

Apr 21st, 2019
582
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.69 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. # File: tar-home.py
  5. # Copyright (c) 2018-2019 Justin Hanekom <justin_hanekom@yahoo.com>
  6. # Licensed under the MIT License
  7.  
  8. # Permission is hereby granted, free of charge, to any person obtaining
  9. # a copy of this software and associated documentation files
  10. # (the "Software"), to deal in the Software without restriction,
  11. # including without limitation the rights to use, copy, modify, merge,
  12. # publish, distribute, sublicense, and/or sell copies of the Software,
  13. # and to permit persons to whom the Software is furnished to do so,
  14. # subject to the following conditions:
  15. #
  16. # The above copyright notice and this permission notice shall be
  17. # included in all copies or substantial portions of the Software.
  18. #
  19. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  20. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  21. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  22. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  23. # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  24. # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  25. # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  26.  
  27. from __future__ import (
  28.     absolute_import, division, print_function, unicode_literals
  29. )
  30. from nine import (
  31.     IS_PYTHON2, basestring, chr, class_types, filter, integer_types,
  32.     implements_iterator, implements_to_string, implements_repr,
  33.     input, iterkeys, iteritems, itervalues, long, map,
  34.     native_str, nine, nimport, range, range_list, reraise, str, zip
  35. )
  36.  
  37. argparse = nimport('argparse')
  38. contextlib = nimport('contextlib')
  39. glob = nimport('glob')
  40. grp = nimport('grp')
  41. os = nimport('os')
  42. pwd = nimport('pwd')
  43. subprocess = nimport('subprocess')
  44. sys = nimport('sys')
  45. time = nimport('time')
  46.  
  47. DEFAULT_PREFIX = 'home_'
  48. DEFAULT_SUFFIX = '.tar.gz'
  49. DEFAULT_KEEP = 5
  50.  
  51.  
  52. def run():
  53.     """Runs this program.
  54.  
  55.    The program generates a new tar file for a given home directory.
  56.    """
  57.     start_time = time.time()
  58.     options = parse_cmd_line(
  59.         default_prefix=DEFAULT_PREFIX,
  60.         default_suffix=DEFAULT_SUFFIX,
  61.         default_keep=DEFAULT_KEEP)
  62.     remove_old_files(
  63.         pattern='{destdir}{sep}{prefix}*{suffix}'.format(
  64.             destdir=options['destdir'],
  65.             sep=os.sep,
  66.             prefix=options['prefix'],
  67.             suffix=options['suffix']),
  68.         keep=options['keep'] - 1, # subtract 1 because new tar will be created
  69.         remove=options['remove'],
  70.         verbose=options['verbose'])
  71.     dest_tar=generate_tar_name(
  72.         dest_dir=options['destdir'],
  73.         prefix=options['prefix'],
  74.         suffix=options['suffix'])
  75.     tar_src_dir(
  76.         src_dir=options['srcdir'],
  77.         dest_tar=dest_tar,
  78.         verbose=options['verbose'])
  79.     change_file_owner_group(
  80.         filename=dest_tar,
  81.         user=options['user'],
  82.         group=options['user'],
  83.         verbose=options['verbose'])
  84.     if options['verbose']:
  85.         print('Done, in {} seconds!'.format(time.time() - start_time))
  86.  
  87.  
  88. def generate_tar_name(**kwargs):
  89.     """Generates a (hopefully) unique fully qualified tar filename.
  90.  
  91.    Arguments:
  92.        kwargs: a dictionary with the following keys:-
  93.            dest_dir: where to save the tar file
  94.            prefix: the prefix to use for the tar file
  95.            suffix: the suffix to use for the tar file
  96.  
  97.    Returns:
  98.        A unique tar name of the format:
  99.            <dest_dir>/<prefix><year><month><day><hour><minute>second><suffix>
  100.        where the year, month, day, etc. values represent the local time
  101.        at the point at which this function is called.
  102.    """
  103.     dest_dir = kwargs.pop('dest_dir')
  104.     prefix = kwargs.pop('prefix')
  105.     suffix = kwargs.pop('suffix')
  106.     if kwargs:
  107.         raise TypeError('Unexpected **kwargs: %r' % kwargs)
  108.     localtime = time.localtime()
  109.     return ('{dest_dir}{sep}{prefix}{year:04d}{month:02d}{day:02d}' +
  110.         '{hour:02d}{minute:02d}{second:02d}{suffix}').format(
  111.             dest_dir=dest_dir,
  112.             prefix=prefix,
  113.             suffix=suffix,
  114.             year=localtime[0],
  115.             month=localtime[1],
  116.             day=localtime[2],
  117.             hour=localtime[3],
  118.             minute=localtime[4],
  119.             second=localtime[5],
  120.             sep=os.sep)
  121.  
  122.  
  123. def parse_cmd_line(**kwargs):
  124.     """Parses the command-line arguments.
  125.  
  126.    Arguments:
  127.        kwargs: a dictionary with the following keys:-
  128.            default_prefix: the default prefix for generated tar files
  129.            default_suffix: the default suffix for generated tar files
  130.            default_keep:   the default number of tar files to keep if removing
  131.                            old tar files
  132.  
  133.    Returns:
  134.        A dictionary with each of the supplied command-line arguments. If
  135.        no value for an item was supplied on the command-line, then the
  136.        default value for that it is returned.
  137.    """
  138.     default_prefix = kwargs.pop('default_prefix')
  139.     default_suffix = kwargs.pop('default_suffix')
  140.     default_keep = kwargs.pop('default_keep')
  141.     if kwargs:
  142.         raise TypeError('Unexpected **kwargs: %r' % kwargs)
  143.     parser = argparse.ArgumentParser(
  144.         description="Archives (tars) the contents of a user's home directory")
  145.     parser.add_argument(
  146.         'user',
  147.         help=' '.join([
  148.             'specify the owner of the archive file;',
  149.             'the archive files user and group will be set to this value']))
  150.     parser.add_argument(
  151.         'srcdir',
  152.         help='specify the owners home directory')
  153.     parser.add_argument(
  154.         'destdir',
  155.         help='specify the directory that the new archive file will be saved to')
  156.     parser.add_argument(
  157.         '--prefix', '-p',
  158.         default=default_prefix,
  159.         help=' '.join([
  160.             'specify the prefix that the newly created archive file will have',
  161.             "(default: '%(default)s')"]))
  162.     parser.add_argument(
  163.         '--suffix', '-x',
  164.         default=default_suffix,
  165.         help=' '.join([
  166.             'specify the suffix that the newly created archive file will have',
  167.             "(default: '%(default)s')"]))
  168.     parser.add_argument(
  169.         '--keep', '-k',
  170.         type=int,
  171.         default=default_keep,
  172.         help='specify how many archives to keep (default: %(default)i)')
  173.     parser.add_argument(
  174.         '--remove', '-r',
  175.         action='store_true',
  176.         default=False,
  177.         help='specify this so that obsolete archives will be removed')
  178.     parser.add_argument(
  179.         '--verbose', '-v',
  180.         action='store_true',
  181.         default=False,
  182.         help='specify this to display verbose output')
  183.     # vars() turns Namespace into a regular dictionary
  184.     options = vars(parser.parse_args())
  185.     options['srcdir'] = chomp_sep(options['srcdir'])
  186.     options['destdir'] = chomp_sep(options['destdir'])
  187.     options['keep'] = max(options['keep'], 1) # keep must be at least 1
  188.     return options
  189.  
  190.  
  191. def chomp_sep(dir_name):
  192.     """Removes any trailing directory separator characters from the given
  193.    directory name.
  194.  
  195.    Arguments:
  196.        dir_name: the name that has to have any trailing slashes removed
  197.  
  198.    Returns:
  199.        The directory name with no trailing separator characters
  200.    """
  201.     while dir_name.endswith(os.sep):
  202.         dir_name = dir_name[:-1]
  203.     return dir_name
  204.  
  205.  
  206. def remove_old_files(**kwargs):
  207.     """Only keeps the newest files that match the given pattern.
  208.  
  209.    Arguments:
  210.        kwargs: a dictionary with the following keys:-
  211.            'pattern'   - the glob pattern of existing files
  212.            'keep'      - the number of existing files to keep
  213.            'remove'    - whether or not to remove old files
  214.            'verbose'   - whether or not to output text describing non-fatal
  215.                          events
  216.  
  217.    Returns:
  218.        None
  219.    """
  220.     pattern = kwargs.pop('pattern')
  221.     keep= kwargs.pop('keep')
  222.     remove = kwargs.pop('remove')
  223.     verbose= kwargs.pop('verbose')
  224.     if kwargs:
  225.         raise TypeError('Unexpected **kwargs: %r' % kwargs)
  226.     if not remove:
  227.         return
  228.     matching = glob.glob(pattern)
  229.     if keep >= len(matching):
  230.         if verbose:
  231.             print("No files matching pattern '{}' need to be removed".format(
  232.                 pattern))
  233.         return
  234.     files = [tup[1] for tup in sorted([(os.path.getmtime(f), f)
  235.                     for f in matching])]
  236.     for f in files[keep:]:
  237.         os.remove(f)
  238.         if verbose:
  239.             print("Removed file '{filename}'".format(filename=f))
  240.  
  241.  
  242. def tar_src_dir(src_dir, dest_tar, verbose):
  243.     """Archives the source directory into a newly created destination tar.
  244.  
  245.    Arguments:
  246.        src_dir:    the source directory to be tarred
  247.        dest_tar:   the name of the tar file to create
  248.        verbose:    whether or not to output text describing non-fatal events
  249.  
  250.    Returns:
  251.        None
  252.   """
  253.     src_dir = os.path.abspath(src_dir)
  254.     index = src_dir.rindex(os.sep)
  255.     base_dir = src_dir[0:index]
  256.     home_dir = src_dir[index+1:]
  257.     if verbose:
  258.         verbose_flag = '--verbose '
  259.     else:
  260.         verbose_flag = ''
  261.     tar_cmd = ''.join(['sudo tar ',
  262.                         '--create --preserve-permissions --atime-preserve ',
  263.                         "--use-compress-program='pigz' ",
  264.                         verbose_flag,
  265.                         "--file={} ".format(dest_tar),
  266.                         home_dir,
  267.                       ])
  268.     with chdir(base_dir):
  269.         if verbose:
  270.             subprocess.call(tar_cmd, shell=True)
  271.         else:
  272.             subprocess.call('{} {}'.format(tar_cmd, '&>/var/null'), shell=True)
  273.     # Ensure that the file is flushed to disk
  274.     subprocess.call('sync; sleep 10; sync', shell=True)
  275.  
  276.  
  277. @contextlib.contextmanager
  278. def chdir(new_dir):
  279.     """Context manager which allows code to run in a different directory.
  280.  
  281.    (1) Stores the current working directory;
  282.    (2) switches to the directory: <new_dir>;
  283.    (3) executes any code run in this context; and then
  284.    (4) returns to the original working directory.
  285.  
  286.    Returns:
  287.        None
  288.    """
  289.     cwd = os.getcwd()
  290.     os.chdir(new_dir)
  291.     try:
  292.         yield
  293.     finally:
  294.         os.chdir(cwd)
  295.  
  296.  
  297. def change_file_owner_group(**kwargs):
  298.     """Changes the owner and group of the named file.
  299.  
  300.    Arguments:
  301.        kwargs: a dictionary with the following keys:-
  302.            'filename': the file whose ownership is to be changed
  303.            'user':     the new owner to assign to the file
  304.            'group':    the new group to assign to the file;
  305.                        this is the same as 'user' if not given
  306.            'verbose':  whether to output text describing non-fatal events
  307.  
  308.    Returns:
  309.        None
  310.    """
  311.     filename = kwargs.pop('filename')
  312.     user = kwargs.pop('user')
  313.     group = kwargs.pop('group', user)
  314.     verbose= kwargs.pop('verbose')
  315.     if kwargs:
  316.         raise TypeError('Unexpected **kwargs: %r' % kwargs)
  317.     try:
  318.         os.chown(filename,
  319.                  pwd.getpwnam(user).pw_uid,
  320.                  grp.getgrnam(group).gr_gid)
  321.         if verbose:
  322.             print("Changed ownership of '{filename}' to {user}:{group}".format(
  323.                 filename=filename,
  324.                 user=user,
  325.                 group=group))
  326.     except os.OSError:
  327.         print("Unable to change ownership of "
  328.             + "'{filename}' to {user}:{group}".format(
  329.                 filename=filename,
  330.                 user=user,
  331.                 group=group),
  332.             file=sys.stderr)
  333.  
  334.  
  335. if __name__ == '__main__':
  336.     run()
  337.  
  338. # vim: set filetype=python smartindent autoindent smarttab expandtab tabstop=4 softtabstop=4 shiftwidth=4 autoread
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement