#!/usr/bin/python3
#
# image-minimizer: removes files and packages on the filesystem
#
# Copyright 2007-2016 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
import glob
import argparse
import os
import sys
import rpm

class ImageMinimizer:
    filename = ''
    dryrun = False
    verbose = False
    prefix = None
    drops = set()
    visited = set()
    drops_rpm = set()
    ts = None

    def __init__(self, filename, root, dryrun, verbose):
        self.filename = filename
        self.prefix = root
        self.dryrun = dryrun
        self.verbose = verbose
        self.ts = None

    # Recursively adds all files and directories.
    # This is done becuase globbing does not allow
    # ** for arbitrary nesting.
    def add_directory(self, files, dirname):
        self.visited.add(dirname)
        for root, dirs, items in os.walk(dirname):
            for d in dirs:
                self.visited.add(os.path.join(root, d))
            for name in items:
                files.add(os.path.join(root, name))

    def add_pattern(self, files, pattern):
        globs = glob.glob(pattern)
        if self.verbose and len(globs) == 0:
            print("%s file not found" % pattern)
        for g in globs:
            if os.path.isdir(g):
                self.add_directory(files, g)
            else:
                files.add(g)

    def add_pattern_rpm(self, rpms, pattern):
        if self.ts is None:
            if self.prefix is None:
                raise Exception('Must specify installation root for droprpm/keeprpm')
            self.ts = rpm.TransactionSet(self.prefix)
        mi = self.ts.dbMatch()
        mi.pattern('name', rpm.RPMMIRE_GLOB, pattern)
        not_found = True
        for hdr in mi:
            not_found = False
            rpms.add(hdr['name'].decode("utf8"))
        if self.verbose and not_found:
            print("%s package not found" % pattern)

    # Parses each line in the ifle
    def parse_line(self, line):
        command = ""
        pattern = ""
        tok = line.split(None,1)
        if len(tok) > 0:
            command = tok[0].lower()
            if len(tok) > 1:
                pattern = tok[1].strip()

        # Strip out all the comments and blank lines
        if not (command.startswith('#') or command==''):
            if command == 'keep':
                if self.prefix is not None :
                    pattern = pattern.lstrip('/')
                    pattern = os.path.join(self.prefix, pattern)
                keeps = set()
                self.add_pattern(keeps, pattern)
                self.drops.difference_update(keeps)
                keeps = None
            elif command == 'drop':
                if self.prefix is not None :
                    pattern = pattern.lstrip('/')
                    pattern = os.path.join(self.prefix, pattern)
                self.add_pattern(self.drops, pattern)
            elif command == 'keeprpm':
                keeps_rpm = set()
                self.add_pattern_rpm(keeps_rpm, pattern)
                self.drops_rpm.difference_update(keeps_rpm)
                keeps_rpm = None
            elif command == 'droprpm':
                self.add_pattern_rpm(self.drops_rpm, pattern)
            else:
                raise Exception('Unknown Command: ' + command)

    def remove(self):
        for tag in sorted(self.drops, reverse=True):
            self.visited.add(os.path.split(tag)[0])
            if os.path.isdir(tag):
                self.visited.add(tag)
            else:
                if self.dryrun or self.verbose:
                    print("rm %s" % tag)
                if not self.dryrun:
                    os.remove(tag)

        #remove all empty directory. Every 8k counts!
        for d in sorted(self.visited, reverse=True):
            if len(os.listdir(d)) == 0:
                if self.dryrun or self.verbose:
                    print("rm -rf %s" % d)
                if not self.dryrun:
                    os.rmdir(d)

    def remove_rpm(self):
        def runCallback(reason, amount, total, key, client_data):
            if self.verbose and reason == rpm.RPMCALLBACK_UNINST_STOP:
                print("%s erased" % key)

        if len(self.drops_rpm) == 0:
            return

        for pkg in self.drops_rpm:
            if self.verbose:
                print("erasing: %s " % pkg)
            self.ts.addErase(pkg)
        if not self.dryrun:
            # skip ts.check(), equivalent to --nodeps
            self.ts.run(runCallback, "erase")

    def filter(self):
        for line in (open(self.filename).readlines()):
            self.parse_line(line.strip())
        self.remove()
        self.remove_rpm()


def parse_options():
    parser = argparse.ArgumentParser(description="Image Minimizer")

    parser.set_defaults(root=os.environ.get('INSTALL_ROOT', '/mnt/sysimage/'), dry_run=False)

    parser.add_argument("-i", "--installroot", metavar="STRING", dest="root",
        help="Root path to prepend to all file patterns and installation root for RPM "
             "operations.  Defaults to INSTALL_ROOT or /mnt/sysimage/")

    parser.add_argument("--dryrun", action="store_true", dest="dryrun",
        help="If set, no filesystem changes are made.")

    parser.add_argument("-v", "--verbose", action="store_true", dest="verbose",
        help="Display every action as it is performed.")

    parser.add_argument("filename", metavar="STRING", help="Filename to process")

    return parser.parse_args()


def main():
    try:
        args = parse_options()
        minimizer = ImageMinimizer(args.filename, args.root, args.dryrun,
                                   args.verbose)
        minimizer.filter()
    except SystemExit as e:
        sys.exit(e.code)
    except KeyboardInterrupt as e:
        print("Aborted at user request")

if __name__ == "__main__":
    main()
