#!/usr/bin/env python

# Prepare and maintain partial trees by architecture
# Copyright (C) 2004  Daniel Silverstone <dsilvers@digital-scurf.org>
# $Id: billie,v 1.4 2004/11/27 16:06:42 troup Exp $

# 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; either version 2 of the License, or
# (at your option) any later version.

# 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 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


###############################################################################
## <kinnison> So Martin, do you have a quote for me yet?
## <tbm> Make something damned stupid up and attribute it to me, that's okay
###############################################################################

import pg, pwd, sys;
import utils, db_access;
import apt_pkg, logging;

from stat import S_ISDIR, S_ISLNK, S_ISREG;
import os;
import cPickle;

## Master path is the main repository
#MASTER_PATH = "/org/ftp.debian.org/scratch/dsilvers/master";

MASTER_PATH = "***Configure Billie::FTPPath Please***";
TREE_ROOT = "***Configure Billie::TreeRootPath Please***";
TREE_DB_ROOT = "***Configure Billie::TreeDatabasePath Please***";
trees = []

###############################################################################
# A BillieTarget is a representation of a target. It is a set of archs, a path
# and whether or not the target includes source.
##################

class BillieTarget:
    def __init__(self, name, archs, source):
        self.name = name;
        self.root = "%s/%s" % (TREE_ROOT,name);
        self.archs = archs.split(",");
        self.source = source;
        self.dbpath = "%s/%s.db" % (TREE_DB_ROOT,name);
        self.db = BillieDB();
        if os.path.exists( self.dbpath ):
            self.db.load_from_file( self.dbpath );

    ## Save the db back to disk
    def save_db(self):
        self.db.save_to_file( self.dbpath );

    ## Returns true if it's a poolish match
    def poolish_match(self, path):
        for a in self.archs:
            if path.endswith( "_%s.deb" % (a) ):
                return 1;
            if path.endswith( "_%s.udeb" % (a) ):
                return 1;
        if self.source:
            if (path.endswith( ".tar.gz" ) or
                path.endswith( ".diff.gz" ) or
                path.endswith( ".dsc" )):
                return 1;
        return 0;

    ## Returns false if it's a badmatch distswise
    def distish_match(self,path):
        for a in self.archs:
            if path.endswith("/Contents-%s.gz" % (a)):
                return 1;
            if path.find("/binary-%s/" % (a)) != -1:
                return 1;
            if path.find("/installer-%s/" % (a)) != -1:
                return 1;
        if path.find("/source/") != -1:
            if self.source:
                return 1;
            else:
                return 0;
        if path.find("/Contents-") != -1:
            return 0;
        if path.find("/binary-") != -1:
            return 0;
        if path.find("/installer-") != -1:
            return 0;
        return 1;
    
##############################################################################
# The applicable function is basically a predicate. Given a path and a
# target object its job is to decide if the path conforms for the
# target and thus is wanted.
#
# 'verbatim' is a list of files which are copied regardless
# it should be loaded from a config file eventually
##################

verbatim = [
    ];

verbprefix = [
    "/tools/",
    "/README",
    "/doc/"
    ];

def applicable(path, target):
    if path.startswith("/pool/"):
        return target.poolish_match(path);
    if (path.startswith("/dists/") or
        path.startswith("/project/experimental/")):
        return target.distish_match(path);
    if path in verbatim:
        return 1;
    for prefix in verbprefix:
        if path.startswith(prefix):
            return 1;
    return 0;


##############################################################################
# A BillieDir is a representation of a tree.
#   It distinguishes files dirs and links
# Dirs are dicts of (name, BillieDir)
# Files are dicts of (name, inode)
# Links are dicts of (name, target)
##############

class BillieDir:
    def __init__(self):
        self.dirs = {};
        self.files = {};
        self.links = {};

##############################################################################
# A BillieDB is a container for a BillieDir...
##############

class BillieDB:
    ## Initialise a BillieDB as containing nothing
    def __init__(self):
        self.root = BillieDir();

    def _internal_recurse(self, path):
        bdir = BillieDir();
        dl = os.listdir( path );
        dl.sort();
        dirs = [];
        for ln in dl:
            lnl = os.lstat( "%s/%s" % (path, ln) );
            if S_ISDIR(lnl[0]):
                dirs.append(ln);
            elif S_ISLNK(lnl[0]):
                bdir.links[ln] = os.readlink( "%s/%s" % (path, ln) );
            elif S_ISREG(lnl[0]):
                bdir.files[ln] = lnl[1];
            else:
                util.fubar( "Confused by %s/%s -- not a dir, link or file" %
                            ( path, ln ) );
        for d in dirs:
            bdir.dirs[d] = self._internal_recurse( "%s/%s" % (path,d) );

        return bdir;

    ## Recurse through a given path, setting the sequence accordingly
    def init_from_dir(self, dirp):
        self.root = self._internal_recurse( dirp );

    ## Load this BillieDB from file
    def load_from_file(self, fname):
        f = open(fname, "r");
        self.root = cPickle.load(f);
        f.close();

    ## Save this BillieDB to a file
    def save_to_file(self, fname):
        f = open(fname, "w");
        cPickle.dump( self.root, f, 1 );
        f.close();

        
##############################################################################
# Helper functions for the tree syncing...
##################

def _pth(a,b):
    return "%s/%s" % (a,b);

def do_mkdir(targ,path):
    if not os.path.exists( _pth(targ.root, path) ):
        os.makedirs( _pth(targ.root, path) );

def do_mkdir_f(targ,path):
    do_mkdir(targ, os.path.dirname(path));

def do_link(targ,path):
    do_mkdir_f(targ,path);
    os.link( _pth(MASTER_PATH, path),
             _pth(targ.root, path));

def do_symlink(targ,path,link):
    do_mkdir_f(targ,path);
    os.symlink( link, _pth(targ.root, path) );

def do_unlink(targ,path):
    os.unlink( _pth(targ.root, path) );

def do_unlink_dir(targ,path):
    os.system( "rm -Rf '%s'" % _pth(targ.root, path) );

##############################################################################
# Reconciling a target with the sourcedb
################

def _internal_reconcile( path, srcdir, targdir, targ ):
    # Remove any links in targdir which aren't in srcdir
    # Or which aren't applicable
    rm = []
    for k in targdir.links.keys():
        if applicable( _pth(path, k), targ ):
            if not srcdir.links.has_key(k):
                rm.append(k);
        else:
            rm.append(k);
    for k in rm:
        #print "-L-", _pth(path,k)
        do_unlink(targ, _pth(path,k))
        del targdir.links[k];
    
    # Remove any files in targdir which aren't in srcdir
    # Or which aren't applicable
    rm = []
    for k in targdir.files.keys():
        if applicable( _pth(path, k), targ ):
            if not srcdir.files.has_key(k):
                rm.append(k);
        else:
            rm.append(k);
    for k in rm:
        #print "-F-", _pth(path,k)
        do_unlink(targ, _pth(path,k))
        del targdir.files[k];

    # Remove any dirs in targdir which aren't in srcdir
    rm = []
    for k in targdir.dirs.keys():
        if not srcdir.dirs.has_key(k):
            rm.append(k);
    for k in rm:
        #print "-D-", _pth(path,k)
        do_unlink_dir(targ, _pth(path,k))
        del targdir.dirs[k];

    # Add/update files
    for k in srcdir.files.keys():
        if applicable( _pth(path,k), targ ):
            if not targdir.files.has_key(k):
                #print "+F+", _pth(path,k)
                do_link( targ, _pth(path,k) );
                targdir.files[k] = srcdir.files[k];
            else:
                if targdir.files[k] != srcdir.files[k]:
                    #print "*F*", _pth(path,k);
                    do_unlink( targ, _pth(path,k) );
                    do_link( targ, _pth(path,k) );
                    targdir.files[k] = srcdir.files[k];

    # Add/update links
    for k in srcdir.links.keys():
        if applicable( _pth(path,k), targ ):
            if not targdir.links.has_key(k):
                targdir.links[k] = srcdir.links[k]; 
                #print "+L+",_pth(path,k), "->", srcdir.links[k]
                do_symlink( targ, _pth(path,k), targdir.links[k] );
            else:
                if targdir.links[k] != srcdir.links[k]:
                    do_unlink( targ, _pth(path,k) );
                    targdir.links[k] = srcdir.links[k];
                    #print "*L*", _pth(path,k), "to ->", srcdir.links[k]
                    do_symlink( targ, _pth(path,k), targdir.links[k] );

    # Do dirs
    for k in srcdir.dirs.keys():
        if not targdir.dirs.has_key(k):
            targdir.dirs[k] = BillieDir();
            #print "+D+", _pth(path,k)
        _internal_reconcile( _pth(path,k), srcdir.dirs[k],
                             targdir.dirs[k], targ );


def reconcile_target_db( src, targ ):
    _internal_reconcile( "", src.root, targ.db.root, targ );

###############################################################################

def load_config():
    global MASTER_PATH
    global TREE_ROOT
    global TREE_DB_ROOT
    global trees

    MASTER_PATH = Cnf["Billie::FTPPath"];
    TREE_ROOT = Cnf["Billie::TreeRootPath"];
    TREE_DB_ROOT = Cnf["Billie::TreeDatabasePath"];
    
    for a in Cnf.ValueList("Billie::BasicTrees"):
        trees.append( BillieTarget( a, "%s,all" % a, 1 ) )

    for n in Cnf.SubTree("Billie::CombinationTrees").List():
        archs = Cnf.ValueList("Billie::CombinationTrees::%s" % n)
        source = 0
        if "source" in archs:
            source = 1
            archs.remove("source")
        archs = ",".join(archs)
        trees.append( BillieTarget( n, archs, source ) );

def do_list ():
    print "Master path",MASTER_PATH
    print "Trees at",TREE_ROOT
    print "DBs at",TREE_DB_ROOT

    for tree in trees:
        print tree.name,"contains",", ".join(tree.archs),
        if tree.source:
            print " [source]"
        else:
            print ""
        
def do_help ():
    print """Usage: billie [OPTIONS]
Generate hardlink trees of certain architectures

  -h, --help                 show this help and exit
  -l, --list                 list the configuration and exit
"""


def main ():
    global Cnf

    Cnf = utils.get_conf()

    Arguments = [('h',"help","Billie::Options::Help"),
                 ('l',"list","Billie::Options::List"),
                 ];

    arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
    Cnf["Billie::Options::cake"] = "";
    Options = Cnf.SubTree("Billie::Options")

    print "Loading configuration..."
    load_config();
    print "Loaded."

    if Options.has_key("Help"):
        do_help();
        return;
    if Options.has_key("List"):
        do_list();
        return;
    

    src = BillieDB()
    print "Scanning", MASTER_PATH
    src.init_from_dir(MASTER_PATH)
    print "Scanned"

    for tree in trees:
        print "Reconciling tree:",tree.name
        reconcile_target_db( src, tree );
        print "Saving updated DB...",
        tree.save_db();
        print "Done"
    
##############################################################################

if __name__ == '__main__':
    main()
