Initial commit.
authorFredrik Tolf <fredrik@dolda2000.com>
Sun, 1 Apr 2018 16:33:10 +0000 (18:33 +0200)
committerFredrik Tolf <fredrik@dolda2000.com>
Sun, 1 Apr 2018 16:33:10 +0000 (18:33 +0200)
tpkg [new file with mode: 0755]

diff --git a/tpkg b/tpkg
new file mode 100755 (executable)
index 0000000..140ed2f
--- /dev/null
+++ b/tpkg
@@ -0,0 +1,358 @@
+#!/usr/bin/python3
+
+import sys, os, bsddb3, struct, getopt, pwd, time, hashlib
+bd = bsddb3.db
+pj = os.path.join
+deadlock = bd.DBLockDeadlockError
+notfound = bd.DBNotFoundError
+
+class txn(object):
+    def __init__(self, env, flags=bd.DB_TXN_WRITE_NOSYNC):
+        self.tx = env.txn_begin(None, flags)
+        self.env = env
+        self.done = False
+
+    def commit(self):
+        self.done = True
+        self.tx.commit(0)
+
+    def abort(self):
+        self.done = True
+        self.tx.abort()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, etype, exc, tb):
+        if not self.done:
+            self.abort()
+        return False
+
+class dbcursor(object):
+    def __init__(self, db, tx):
+        self.bk = db.cursor(txn=tx.tx)
+
+    def __enter__(self):
+        return self.bk
+
+    def __exit__(self, *args):
+        self.bk.close()
+        return False
+
+def txnfun(envfun):
+    def fxf(fun):
+        def wrapper(self, *args, tx=None, **kwargs):
+            if tx is None:
+                while True:
+                    try:
+                        with txn(envfun(self)) as ltx:
+                            ret = fun(self, *args, tx=ltx, **kwargs)
+                            ltx.commit()
+                            return ret
+                    except deadlock:
+                        continue
+            else:
+                return fun(self, *args, tx=tx, **kwargs)
+        return wrapper
+    return fxf
+
+class prefix(object):
+    use = None
+
+    def __init__(self, root, envdir):
+        self.root = root
+        self.envdir = envdir
+        self._env = None
+        self.dbs = {}
+
+    @property
+    def env(self):
+        if self._env is None:
+            if not os.path.isdir(self.envdir):
+                sys.stderr.write("tpkg: creatings %s...\n" % (self.envdir))
+                os.makedirs(self.envdir)
+            env = bd.DBEnv()
+            env.set_lk_detect(bd.DB_LOCK_RANDOM)
+            fl = bd.DB_THREAD | bd.DB_INIT_MPOOL | bd.DB_INIT_LOCK | bd.DB_INIT_LOG | bd.DB_INIT_TXN | bd.DB_CREATE
+            mode = 0o666
+            env.open(self.envdir, fl, mode)
+            self._env = env
+        return self._env
+
+    def maint(self):
+        env = self._env
+        if env is not None:
+            env.txn_checkpoint(1024)
+            env.log_archive(bd.DB_ARCH_REMOVE)
+
+    @txnfun(lambda self: self.env)
+    def db(self, name, *, dup=False, tx):
+        if name not in self.dbs:
+            db = bd.DB(self.env)
+            if dup:
+                db.set_flags(bd.DB_DUPSORT)
+            fl = bd.DB_THREAD | bd.DB_CREATE
+            mode = 0o666
+            db.open(name, None, bd.DB_BTREE, fl, mode, txn=tx.tx)
+            self.dbs[name] = db
+        return self.dbs[name]
+
+    def close(self):
+        if self._env is not None:
+            for db in self.dbs.values():
+                db.close()
+            self._env.close()
+            self._env = None
+
+    def __del__(self):
+        self.close()
+
+    @txnfun(lambda self: self.env)
+    def unregfile(self, path, *, tx):
+        epath = path.encode("utf-8")
+        db = self.db("filedata")
+        if db.has_key(epath, txn=tx.tx):
+            db.delete(epath, txn=tx.tx)
+        db = self.db("file-pkg")
+        epkg = db.get(epath, None, txn=tx.tx)
+        if epkg is not None:
+            db.delete(epath, txn=tx.tx)
+            with dbcursor(self.db("pkg-file", dup=True), tx) as cur:
+                try:
+                    cur.get_both(epkg, epath)
+                except notfound:
+                    pass
+                else:
+                    cur.delete()
+
+    @txnfun(lambda self: self.env)
+    def regfile(self, path, pkg, digest, *, tx):
+        epath, epkg = path.encode("utf-8"), pkg.encode("utf-8")
+        self.unregfile(path, tx=tx)
+        filedata = b"digest\0" + digest.encode("utf-8") + b"\0"
+        self.db("filedata").put(epath, filedata, txn=tx.tx)
+        self.db("file-pkg").put(epath, epkg, txn=tx.tx)
+        self.db("pkg-file", dup=True).put(epkg, epath, flags=bd.DB_NODUPDATA, txn=tx.tx)
+
+    @txnfun(lambda self: self.env)
+    def filedata(self, path, default=KeyError, *, tx):
+        epath = path.encode("utf-8")
+        data = self.db("filedata").get(epath, None, txn=tx.tx)
+        if data is None:
+            if default is KeyError:
+                raise KeyError(path)
+            else:
+                return default
+        data = data.split(b'\0')
+        if data[-1] != b"" or len(data) % 2 != 1:
+            raise Exception("invalid filedata")
+        ret = {}
+        for i in range(0, len(data) - 1, 2):
+            ret[data[i].decode("utf-8")] = data[i + 1].decode("utf-8")
+        return ret
+
+    @txnfun(lambda self: self.env)
+    def filepkg(self, path, default=KeyError, *, tx):
+        epath = path.encode("utf-8")
+        epkg = self.db("file-pkg").get(epath, None, txn=tx.tx)
+        if epkg is None:
+            if default is KeyError:
+                raise KeyError(path)
+            else:
+                return default
+        return epkg.decode("utf-8")
+
+    @txnfun(lambda self: self.env)
+    def pkgfiles(self, pkg, default=KeyError, *, tx):
+        epkg = pkg.encode("utf-8")
+        with dbcursor(self.db("pkg-file", dup=True), tx) as cur:
+            try:
+                edat = cur.set(epkg)
+                if edat is None:
+                    raise notfound()
+                fpkg, epath = edat
+                assert fpkg == epkg
+            except notfound:
+                if default is KeyError:
+                    raise KeyError(pkg)
+                else:
+                    return default
+            ret = []
+            while fpkg == epkg:
+                ret.append(epath.decode("utf-8"))
+                edat = cur.next()
+                if edat is None:
+                    break
+                fpkg, epath = edat
+        return ret
+
+    @classmethod
+    def home(cls):
+        home = pwd.getpwuid(os.getuid()).pw_dir
+        return cls(pj(home, "sys"), pj(home, ".tpkg/db"))
+
+    @classmethod
+    def test(cls):
+        home = pwd.getpwuid(os.getuid()).pw_dir
+        return cls(pj(home, "tpkgtest"), pj(home, ".tpkg/testdb"))
+
+    @classmethod
+    def local(cls):
+        return cls("/usr/local", "/usr/local/etc/tpkg/db")
+
+class vfsfile(object):
+    def __init__(self, path, fullpath):
+        self.path = path
+        self.fullpath = fullpath
+
+    def open(self):
+        return open(self.fullpath, "rb")
+
+    def stat(self):
+        return os.stat(self.fullpath)
+
+class vfspkg(object):
+    def __init__(self, root):
+        self.root = root
+
+    def __iter__(self):
+        def scan(lp, fp):
+            dpre = "" if (lp is "") else lp + "/"
+            for dent in os.scandir(fp):
+                dpath = dpre + dent.name
+                if dent.is_dir():
+                    yield from scan(dpath, dent.path)
+                else:
+                    yield vfsfile(dpath, dent.path)
+        return scan("", self.root)
+
+def copy(dst, src):
+    dig = hashlib.sha256()
+    while True:
+        buf = src.read(65536)
+        if buf == b"":
+            return dig.hexdigest().lower()
+        dst.write(buf)
+        dig.update(buf)
+
+def digest(fp):
+    dig = hashlib.sha256()
+    while True:
+        buf = fp.read(65536)
+        if buf == b"":
+            return dig.hexdigest().lower()
+        dig.update(buf)
+
+def install(pfx, pkg, pkgname):
+    for fl in pkg:
+        if os.path.exists(pj(pfx.root, fl.path)):
+            sys.stderr.write("tpkg: %s: already exists\n" % (fl.path))
+            sys.exit(1)
+    for fl in pkg:
+        tp = pj(pfx.root, fl.path)
+        tpdir = os.path.dirname(tp)
+        if not os.path.isdir(tpdir):
+            os.makedirs(tpdir)
+        tmpp = tp + ".tpkg-new"
+        sb = fl.stat()
+        with open(tmpp, "wb") as ofp:
+            os.fchmod(ofp.fileno(), sb.st_mode & 0o7777)
+            with fl.open() as ifp:
+                dig = copy(ofp, ifp)
+        pfx.regfile(fl.path, pkgname, dig)
+        os.rename(tmpp, tp)
+        os.utime(tp, ns=(time.time(), sb.st_mtime))
+
+def uninstall(pfx, pkg):
+    for fn in pfx.pkgfiles(pkg):
+        fpath = pj(pfx.root, fn)
+        if not os.path.exists(fpath):
+            sys.stderr.write("tpkg: warning: %s does not exist\n" % (fn))
+        else:
+            fdat = pfx.filedata(fn)
+            with open(fpath, "rb") as fp:
+                if digest(fp) != fdat.get("digest", ""):
+                    sys.stderr.write("tpkg: %s does not match registered hash\n" % (fn))
+                    sys.exit(1)
+    for fn in pfx.pkgfiles(pkg):
+        fpath = pj(pfx.root, fn)
+        try:
+            os.unlink(fpath)
+        except FileNotFoundError:
+            pass
+        pfx.unregfile(fn)
+
+cmds = {}
+
+def cmd_install(argv):
+    def usage(out):
+        out.write("usage: tpkg install [-n NAME] SOURCEDIR\n")
+    opts, args = getopt.getopt(argv, "n:")
+    pkgname = None
+    for o, a in opts:
+        if o == "-n":
+            pkgname = a
+    if len(args) < 1:
+        usage(sys.stderr)
+        sys.exit(1)
+    srcpath = args[0]
+    if not os.path.isdir(srcpath):
+        sys.stderr.write("tpkg: %s: not a directory\n" % (srcpath))
+        sys.exit(1)
+    if pkgname is None:
+        pkgname = os.path.basename(os.path.realpath(srcpath))
+    if not pkgname:
+        sys.stderr.write("tpkg: could not determine package name\n")
+        sys.exit(1)
+    install(prefix.use, vfspkg(srcpath), pkgname)
+cmds["install"] = cmd_install
+
+def cmd_uninstall(argv):
+    def usage(out):
+        out.write("usage: tpkg uninstall NAME\n")
+    opts, args = getopt.getopt(argv, "")
+    if len(args) < 1:
+        usage(sys.stderr)
+        sys.exit(1)
+    pkgname = args[0]
+    uninstall(prefix.use, pkgname)
+cmds["uninstall"] = cmd_uninstall
+
+def usage(file):
+    file.write("usage:\ttpkg help\n")
+cmds["help"] = lambda argv: usage(sys.stdout)
+
+def main(argv):
+    pfx = None
+    opts, args = getopt.getopt(argv, "hp:")
+    for o, a in opts:
+        if o == "-h":
+            usage(sys.stdout)
+            sys.exit(0)
+        elif o == "-p":
+            if a == "sys":
+                pfx = prefix.home()
+            elif a == "local":
+                pfx = prefix.local()
+            elif a == "test":
+                pfx = prefix.test()
+            else:
+                sys.stderr.write("tpkg: %s: undefined prefix\n" % (a))
+                sys.exit(1)
+    if pfx is None:
+        sys.stderr.write("tpkg: no prefix specified\n")
+        sys.exit(1)
+    prefix.use = pfx
+    try:
+        if len(args) > 0 and args[0] in cmds:
+            cmds[args[0]](args[1:])
+            pfx.maint()
+            sys.exit(0)
+        else:
+            usage(sys.stderr)
+            sys.exit(1)
+    finally:
+        pfx.close()
+
+if __name__ == "__main__":
+    main(sys.argv[1:])