From 82bfc891550525b31563adfca69115586ed19540 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Sun, 1 Apr 2018 18:33:10 +0200 Subject: [PATCH] Initial commit. --- tpkg | 358 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100755 tpkg diff --git a/tpkg b/tpkg new file mode 100755 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:]) -- 2.11.0