#!/usr/bin/python3
"""
usage: %s [-v] user group dirperm fileperm [dirs/files] ...
       -v verbose level (cumulative)
       -r recursive behaviour
       -l follow symlinks
       -S simulate action
       user/group: user/group (numeric/symbolic) to apply or - to ignore
       dirperm/fileperm: _octal_ permissions to apply or - to ignore
       dirs/files: to change, default=.
"""
# (c)reated by Hans-Peter Jansen, LISA GmbH, Berlin
#
# Licence:      GPL   http://www.gnu.org/licenses/gpl.html
#
# 2001-11-23    hp    initial version
# 2001-11-24    hp    skip unnecessary operations, simulation run
#                     class based rewrite, cleanup
# 2002-01-12    hp    minor fix in usage
# 2019-02-14    hp    don't follow directory symlinks, if -l is not given
#
# TODO:
# - stay in filesystem option
# - maxdepth option
# - symbolic permissions
# - testing
#
# vim:set et ts=4 sw=4:

import sys, os
import getopt
import pwd, grp
from stat import *

verbose = 0
argv0 = os.path.basename(sys.argv[0])

def out(lvl, arg):
    if lvl > verbose:
        return
    err(arg, sys.stdout)

def err(arg, ch = sys.stderr):
    if arg:
        ch.write(arg)
        if arg[-1] != "\n":
            ch.write("\n")
    else:
        ch.write("\n")
    ch.flush()

def exit(ret=0, arg=None):
    if arg:
        out(0, "%s: %s" % (argv0, arg))
    sys.exit(ret)

def usage(ret=0, arg=None):
    if arg:
        out(0, "%s: %s" % (argv0, arg))
    out(0, __doc__ % (argv0))
    sys.exit(ret)

class chPerm:
    """ chown/grp/mod manipulation class """
    # parameter
    uid = None
    gid = None
    dperm = None
    fperm = None
    # options
    recursive = 0
    fsymlink = 0
    simulate = 0

    def stat(self, path):
        self.mode = None
        try:
            if self.fsymlink:
                self.mode = os.stat(path)
            else:
                self.mode = os.lstat(path)
        except:
            err("cannot stat: %s" % (sys.exc_value))
        return self.mode

    def chownmod(self, path):
        """ change owner/gid/permissions of one file """
        # check access in simualion mode
        if self.simulate:
            if not os.access(path, os.W_OK):
                err("access denied: %s" % path)
                return
        if not self.mode:   # sanity check
            return
        if S_ISDIR(self.mode[ST_MODE]):
            perm = self.dperm
        elif S_ISREG(self.mode[ST_MODE]):
            perm = self.fperm
        else:
            out(2, "ignored: %s" % p)
            return
        # check for chmod action
        if perm:
            # check if already in demanded state
            if S_IMODE(self.mode[ST_MODE]) == perm:
                out(3, "chmod %o %s skipped" % (perm, path))
            else:
                if verbose > 1:
                    out(2, "chmod %o -> %o %s" % (S_IMODE(self.mode[ST_MODE]), perm, path))
                else:
                    out(1, "chmod %o %s" % (perm, path))
                if not self.simulate:
                    try:
                        os.chmod(path, perm)
                    except:
                        err("cannot chmod: %s" % (sys.exc_value))
        # check for chown/grp actions
        uid, gid = self.uid, self.gid
        if uid != None or gid != None:
            # at least one of them should change
            # keep ignored values (if any)
            if uid == None:
                uid = self.mode[ST_UID]
            elif gid == None:
                gid = self.mode[ST_GID]
            # check if already in demanded state
            if uid == self.mode[ST_UID] and gid == self.mode[ST_GID]:
                out(3, "chown %d:%d %s skipped" % (uid, gid, path))
            else:
                if verbose > 1:
                    out(2, "chown %d:%d -> %d:%d %s" % (self.mode[ST_UID], self.mode[ST_GID],
                                                        uid, gid, path))
                else:
                    out(1, "chown %d:%d %s" % (uid, gid, path))
                if not self.simulate:
                    try:
                        os.chown(path, uid, gid)
                    except:
                        err("cannot chown: %s" % (sys.exc_value))

    def chperm(self, path):
        """ recursively chown/grp/mod dirs/files """
        if not self.stat(path):     # check for existence
            return
        self.chownmod(path)
        if S_ISDIR(self.mode[ST_MODE]):
            for f in os.listdir(path):      # iterate over all items
                p = os.path.join(path, f)
                if not self.stat(p):
                    continue                # oups
                if self.recursive and S_ISDIR(self.mode[ST_MODE]):
                    self.chperm(p)
                else:
                    self.chownmod(p)

def normarg(arg, radix = 10):
    """ normalize command line args """
    if arg == '-':
        arg = None
    else:
        try:
            arg = int(arg, radix)
        except:
            pass
    return arg

if __name__ == '__main__':
    try:
        optlist, args = getopt.getopt(sys.argv[1:], "vrlS")
    except getopt.error as msg:
        usage(1, msg)
    # process options
    chp = chPerm()
    for opt, par in optlist:
        if opt == '-r':
            chp.recursive = 1
        elif opt == '-l':
            chp.fsymlink = 1
        elif opt == '-S':
            chp.simulate = 1
            if not verbose:     # ensure verbosity
                verbose = 1
        elif opt == '-v':
            verbose += 1
    # process standard args
    if len(args) < 4:
        usage(1)
    uid, gid, dperm, fperm  = args[:4]
    # process targets
    args = args[4:]
    if not args:
        args.append(".")
    # prepare user
    uid = normarg(uid)
    if type(uid) == type(""):
        try:
            uid = pwd.getpwnam(uid)[2]
        except:
            exit(2, "user %s not known" % uid)
    if uid and not pwd.getpwuid(uid):
        exit(2, "uid %s not valid" % uid)
    chp.uid = uid
    # prepare group
    gid = normarg(gid)
    if type(gid) == type(""):
        try:
            gid = grp.getgrnam(gid)[2]
        except:
            exit(3, "group %s not known" % gid)
    if gid and not grp.getgrgid(gid):
        exit(3, "gid %s not valid" % gid)
    chp.gid = gid
    # prepare permissions
    chp.dperm = normarg(dperm, 8)
    chp.fperm = normarg(fperm, 8)
    # check, if something to do
    if chp.uid or chp.gid or chp.dperm or chp.fperm:
        for p in args:
            chp.chperm(p)
    else:
        exit(4, "are you kidding?!?")

