teichmann@535: #!/usr/bin/env python teichmann@535: # -*- coding: UTF-8 -*- teichmann@535: # teichmann@535: # Copyright (C) 2011 by Intevation GmbH teichmann@535: # Authors: teichmann@535: # Sascha L. Teichmann teichmann@535: # teichmann@535: # This program is free software under the GPL (>=v2) teichmann@535: # Read the file COPYING coming with the software for details. teichmann@535: teichmann@535: import sys teichmann@535: import os teichmann@535: import re teichmann@535: teichmann@535: import subprocess teichmann@535: import logging teichmann@535: teichmann@535: from heapq import nsmallest teichmann@535: teichmann@535: from optparse import OptionParser teichmann@535: teichmann@535: log = logging.getLogger(__name__) teichmann@535: log.setLevel(logging.WARNING) teichmann@535: log.addHandler(logging.StreamHandler(sys.stderr)) teichmann@535: teichmann@535: DEFAULT_KEEP = 3 teichmann@535: teichmann@535: FIELD = re.compile("([a-zA-Z]+):\s*(.+)") teichmann@535: teichmann@535: # map rich comparison to 'dpkg --compare-versions' teichmann@535: # map == to !=, < to >= and so on to reverse order in heap. teichmann@535: RICH_CMP = dict([ teichmann@535: ("__%s__" % a, lambda se, ot: teichmann@535: subprocess.call([ teichmann@535: "dpkg", "--compare-versions", teichmann@535: se.version, b, ot.version]) == 0) teichmann@535: for a, b in (("eq", "ne"), ("ne", "eq"), teichmann@535: ("lt", "ge"), ("gt", "le"), teichmann@535: ("le", "gt"), ("ge", "lt"))]) teichmann@535: teichmann@535: teichmann@535: class DebCmp(object): teichmann@535: """Helper class to make deb files comparable teichmann@535: by there versions. teichmann@535: """ teichmann@535: teichmann@535: def __init__(self, version, path): teichmann@535: self.version = version teichmann@535: self.path = path teichmann@535: teichmann@535: self.__dict__.update(RICH_CMP) teichmann@535: teichmann@535: teichmann@535: def deb_info(deb, fields=["Package", "Version"]): teichmann@535: """Extract some meta info from a deb file.""" teichmann@535: po = subprocess.Popen( teichmann@535: ["dpkg-deb", "-f", deb] + fields, teichmann@535: stdout=subprocess.PIPE) teichmann@535: out = po.communicate()[0] teichmann@535: return dict([m.groups() teichmann@535: for m in map(FIELD.match, out.splitlines()) if m]) teichmann@535: teichmann@535: teichmann@535: def oldest_debs(deb_dir, keep=DEFAULT_KEEP): teichmann@535: """Given directory containing deb files this function teichmann@535: returns the files that are older than the youngest teichmann@535: keep-th per package. teichmann@535: """ teichmann@535: teichmann@535: log.info("scanning dir '%s'" % deb_dir) teichmann@535: teichmann@535: packages = {} teichmann@535: teichmann@535: num = 1 teichmann@535: for f in os.listdir(deb_dir): teichmann@535: if not f.endswith(".deb"): continue teichmann@535: deb = os.path.join(deb_dir, f) teichmann@535: if not os.path.isfile(deb): continue teichmann@535: info = deb_info(deb) teichmann@535: packages.setdefault(info['Package'], []).append( teichmann@535: DebCmp(info['Version'], deb)) teichmann@535: if (num % 10) == 0: teichmann@535: log.info("%d debs found" % (num-1)) teichmann@535: num += 1 teichmann@535: teichmann@535: if log.isEnabledFor(logging.INFO): teichmann@535: log.info("%d debs found" % (num-1)) teichmann@535: log.info("number packages: %s" % len(packages)) teichmann@535: teichmann@535: for package, debs in packages.iteritems(): teichmann@535: if len(debs) > keep: teichmann@535: # full sorting is not required teichmann@535: stay = frozenset([d.path for d in nsmallest(keep, debs)]) teichmann@535: teichmann@535: for deb in debs: teichmann@535: if deb.path not in stay: teichmann@535: yield deb.path teichmann@535: teichmann@535: teichmann@535: def main(): teichmann@535: usage = "usage: %prog [options] dir ..." teichmann@535: parser = OptionParser(usage=usage) teichmann@535: parser.add_option( teichmann@535: "-v", "--verbose", action="store_true", teichmann@535: dest="verbose", teichmann@535: help="verbose output") teichmann@535: parser.add_option( teichmann@535: "-d", "--dry-run", action="store_true", teichmann@535: dest="dry_run", teichmann@535: help="don't remove the old deb files") teichmann@535: parser.add_option( teichmann@535: "-k", "--keep", action="store", teichmann@535: dest="keep", type="int", default=DEFAULT_KEEP, teichmann@535: help="number of files to keep. Default: %d" % DEFAULT_KEEP) teichmann@535: teichmann@535: options, args = parser.parse_args() teichmann@535: teichmann@535: remove = options.dry_run and (lambda x: None) or os.remove teichmann@535: keep = max(1, options.keep) teichmann@535: if options.verbose: log.setLevel(logging.INFO) teichmann@535: teichmann@535: for deb_dir in args: teichmann@535: teichmann@535: if not os.path.isdir(deb_dir): teichmann@535: log.warn("'%s' is not a directory" % deb_dir) teichmann@535: continue teichmann@535: teichmann@535: for deb in oldest_debs(deb_dir, keep): teichmann@535: log.debug("remove '%s'" % deb) teichmann@535: remove(deb) teichmann@535: changes = deb.path[:-3] + "changes" teichmann@535: if os.path.isfile(changes): teichmann@535: log.debug("remove '%s'" % changes) teichmann@535: remove(changes) teichmann@535: teichmann@535: teichmann@535: if __name__ == "__main__": teichmann@535: main()