#!/usr/bin/python

# vim: set tabstop=4 shiftwidth=4 syntax=python expandtab:

import errno, optparse, os, shutil, sys, tempfile

#             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
#                     Version 2, December 2004
# 
#  Copyright (C) 2004 Sam Hocevar
#   14 rue de Plaisance, 75014 Paris, France
#  Everyone is permitted to copy and distribute verbatim or modified
#  copies of this license document, and changing it is allowed as long
#  as the name is changed.
# 
#             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
#    TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
# 
#   0. You just DO WHAT THE FUCK YOU WANT TO.

class HardenMode:
    LINK = 0
    COPY = 1

encounteredCrossDeviceLink = False

def main():
    usage = """%prog [options] file...

hardens symlinks by either copying the symlink destination to the original
file, or creating an extra hard link to the original file.  either way will
delete the original symlink in the process.

directories specified on the command-line will be recursively chased. copied
files/directories will retain original metadata (timestamps etc)."""

    optionParser = optparse.OptionParser(usage=usage)
    optionParser.add_option("-l", "--link", action="store_const",
            const=HardenMode.LINK, dest="hardenMode",
            help=("hard-links the original file (default behaviour)"))
    optionParser.add_option("-c", "--copy", action="store_const",
            const=HardenMode.COPY, dest="hardenMode",
            help=("copies the original file"))
    optionParser.add_option("-v", "--verbose", action="count",
            dest="verbosity",
            help=("increases verbosity"))
    optionParser.add_option("-n", "--dry-run", action="store_true",
            dest="dryRun",
            help=("don't actually perform the operations, just print " \
                "them (implies -v)"))

    (options, argvRemainder) = optionParser.parse_args()

    if not options.hardenMode: options.hardenMode = HardenMode.LINK
    if options.dryRun and not options.verbosity:
        options.verbosity = 1

    if len(argvRemainder) == 0:
        optionParser.print_help()
        return

    for arg in argvRemainder:
        hardenPath(arg, options.hardenMode, options.verbosity, os.getcwd(),
                options.dryRun)

    if encounteredCrossDeviceLink:
        print >> sys.stderr, "(use -f flag to force cross-device links)"

def hardenPath(path, mode, verbosity=0, cwd=None, dryRun=False):
    if cwd == None: cwd = os.getcwd()

    if os.path.isdir(path):
        if verbosity >= 1:
            print "cd %s" % path
        for file in sorted(os.listdir(path)):
            hardenPath(os.path.join(path, file), mode, verbosity, cwd, dryRun)
    elif os.path.islink(path):
        symlinkDestination = os.readlink(path)
        absoluteSymlinkDestination = os.path.join(os.path.dirname(path),
                symlinkDestination)
        if not os.path.exists(absoluteSymlinkDestination):
            print "warning: path at %s is a broken symlink to %s, " \
                    "skipping" % (path, symlinkDestination)
            return
        
        (dirname, basename) = os.path.split(path)

        (tmpFd, tmpPath) = tempfile.mkstemp(".harden", ".%s." % basename,
                dirname)
        os.close(tmpFd)
        os.unlink(tmpPath)

        try:
            if mode == HardenMode.LINK:
                if not dryRun:
                    os.link(absoluteSymlinkDestination, tmpPath)
            elif mode == HardenMode.COPY:
                if not dryRun:
                    shutil.copy2(absoluteSymlinkDestination, tmpPath)
        except OSError, (e):
            if mode == HardenMode.LINK and e.errno == errno.EXDEV:
                encounteredCrossDeviceLink = True
                print >> sys.stderr, "warning: %s is a cross-device link, " \
                        "skipping" % path
            else:
                print >> sys.stderr, "warning: %s: %s" % (path, e.strerror)
            return

        if not dryRun:
            if not os.path.exists(tmpPath):
                print >> sys.stderr, "couldn't create tmp path at %s" % tmpPath
                sys.exit(1)

            os.unlink(path)
            os.rename(tmpPath, path)

        if verbosity >= 1:
            if mode == HardenMode.LINK:
                print "ln %s %s" % (symlinkDestination, path)
            if mode == HardenMode.COPY:
                print "cp %s %s" % (symlinkDestination, path)

    else:
        # neither a directory nor a symlink; skip
        pass

if __name__ == "__main__":
    main()

