#!/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()