| 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | import sys, os, bsddb3, struct, getopt, pwd, time, hashlib |
| 4 | bd = bsddb3.db |
| 5 | pj = os.path.join |
| 6 | deadlock = bd.DBLockDeadlockError |
| 7 | notfound = bd.DBNotFoundError |
| 8 | |
| 9 | class txn(object): |
| 10 | def __init__(self, env, flags=bd.DB_TXN_WRITE_NOSYNC): |
| 11 | self.tx = env.txn_begin(None, flags) |
| 12 | self.env = env |
| 13 | self.done = False |
| 14 | |
| 15 | def commit(self): |
| 16 | self.done = True |
| 17 | self.tx.commit(0) |
| 18 | |
| 19 | def abort(self): |
| 20 | self.done = True |
| 21 | self.tx.abort() |
| 22 | |
| 23 | def __enter__(self): |
| 24 | return self |
| 25 | |
| 26 | def __exit__(self, etype, exc, tb): |
| 27 | if not self.done: |
| 28 | self.abort() |
| 29 | return False |
| 30 | |
| 31 | class dbcursor(object): |
| 32 | def __init__(self, db, tx): |
| 33 | self.bk = db.cursor(txn=tx.tx) |
| 34 | |
| 35 | def __enter__(self): |
| 36 | return self.bk |
| 37 | |
| 38 | def __exit__(self, *args): |
| 39 | self.bk.close() |
| 40 | return False |
| 41 | |
| 42 | def txnfun(envfun): |
| 43 | def fxf(fun): |
| 44 | def wrapper(self, *args, tx=None, **kwargs): |
| 45 | if tx is None: |
| 46 | while True: |
| 47 | try: |
| 48 | with txn(envfun(self)) as ltx: |
| 49 | ret = fun(self, *args, tx=ltx, **kwargs) |
| 50 | ltx.commit() |
| 51 | return ret |
| 52 | except deadlock: |
| 53 | continue |
| 54 | else: |
| 55 | return fun(self, *args, tx=tx, **kwargs) |
| 56 | return wrapper |
| 57 | return fxf |
| 58 | |
| 59 | class prefix(object): |
| 60 | use = None |
| 61 | |
| 62 | def __init__(self, root, envdir): |
| 63 | self.root = root |
| 64 | self.envdir = envdir |
| 65 | self._env = None |
| 66 | self.dbs = {} |
| 67 | |
| 68 | @property |
| 69 | def env(self): |
| 70 | if self._env is None: |
| 71 | if not os.path.isdir(self.envdir): |
| 72 | sys.stderr.write("tpkg: creatings %s...\n" % (self.envdir)) |
| 73 | os.makedirs(self.envdir) |
| 74 | env = bd.DBEnv() |
| 75 | env.set_lk_detect(bd.DB_LOCK_RANDOM) |
| 76 | fl = bd.DB_THREAD | bd.DB_INIT_MPOOL | bd.DB_INIT_LOCK | bd.DB_INIT_LOG | bd.DB_INIT_TXN | bd.DB_CREATE |
| 77 | mode = 0o666 |
| 78 | env.open(self.envdir, fl, mode) |
| 79 | self._env = env |
| 80 | return self._env |
| 81 | |
| 82 | def maint(self): |
| 83 | env = self._env |
| 84 | if env is not None: |
| 85 | env.txn_checkpoint(1024) |
| 86 | env.log_archive(bd.DB_ARCH_REMOVE) |
| 87 | |
| 88 | @txnfun(lambda self: self.env) |
| 89 | def db(self, name, *, dup=False, tx): |
| 90 | if name not in self.dbs: |
| 91 | db = bd.DB(self.env) |
| 92 | if dup: |
| 93 | db.set_flags(bd.DB_DUPSORT) |
| 94 | fl = bd.DB_THREAD | bd.DB_CREATE |
| 95 | mode = 0o666 |
| 96 | db.open(name, None, bd.DB_BTREE, fl, mode, txn=tx.tx) |
| 97 | self.dbs[name] = db |
| 98 | return self.dbs[name] |
| 99 | |
| 100 | def close(self): |
| 101 | if self._env is not None: |
| 102 | for db in self.dbs.values(): |
| 103 | db.close() |
| 104 | self._env.close() |
| 105 | self._env = None |
| 106 | |
| 107 | def __del__(self): |
| 108 | self.close() |
| 109 | |
| 110 | @txnfun(lambda self: self.env) |
| 111 | def unregfile(self, path, *, tx): |
| 112 | epath = path.encode("utf-8") |
| 113 | db = self.db("filedata") |
| 114 | if db.has_key(epath, txn=tx.tx): |
| 115 | db.delete(epath, txn=tx.tx) |
| 116 | db = self.db("file-pkg") |
| 117 | epkg = db.get(epath, None, txn=tx.tx) |
| 118 | if epkg is not None: |
| 119 | db.delete(epath, txn=tx.tx) |
| 120 | with dbcursor(self.db("pkg-file", dup=True), tx) as cur: |
| 121 | try: |
| 122 | cur.get_both(epkg, epath) |
| 123 | except notfound: |
| 124 | pass |
| 125 | else: |
| 126 | cur.delete() |
| 127 | |
| 128 | @txnfun(lambda self: self.env) |
| 129 | def regfile(self, path, pkg, digest, *, tx): |
| 130 | epath, epkg = path.encode("utf-8"), pkg.encode("utf-8") |
| 131 | self.unregfile(path, tx=tx) |
| 132 | filedata = b"digest\0" + digest.encode("utf-8") + b"\0" |
| 133 | self.db("filedata").put(epath, filedata, txn=tx.tx) |
| 134 | self.db("file-pkg").put(epath, epkg, txn=tx.tx) |
| 135 | self.db("pkg-file", dup=True).put(epkg, epath, flags=bd.DB_NODUPDATA, txn=tx.tx) |
| 136 | |
| 137 | @txnfun(lambda self: self.env) |
| 138 | def filedata(self, path, default=KeyError, *, tx): |
| 139 | epath = path.encode("utf-8") |
| 140 | data = self.db("filedata").get(epath, None, txn=tx.tx) |
| 141 | if data is None: |
| 142 | if default is KeyError: |
| 143 | raise KeyError(path) |
| 144 | else: |
| 145 | return default |
| 146 | data = data.split(b'\0') |
| 147 | if data[-1] != b"" or len(data) % 2 != 1: |
| 148 | raise Exception("invalid filedata") |
| 149 | ret = {} |
| 150 | for i in range(0, len(data) - 1, 2): |
| 151 | ret[data[i].decode("utf-8")] = data[i + 1].decode("utf-8") |
| 152 | return ret |
| 153 | |
| 154 | @txnfun(lambda self: self.env) |
| 155 | def filepkg(self, path, default=KeyError, *, tx): |
| 156 | epath = path.encode("utf-8") |
| 157 | epkg = self.db("file-pkg").get(epath, None, txn=tx.tx) |
| 158 | if epkg is None: |
| 159 | if default is KeyError: |
| 160 | raise KeyError(path) |
| 161 | else: |
| 162 | return default |
| 163 | return epkg.decode("utf-8") |
| 164 | |
| 165 | @txnfun(lambda self: self.env) |
| 166 | def pkgfiles(self, pkg, default=KeyError, *, tx): |
| 167 | epkg = pkg.encode("utf-8") |
| 168 | with dbcursor(self.db("pkg-file", dup=True), tx) as cur: |
| 169 | try: |
| 170 | edat = cur.set(epkg) |
| 171 | if edat is None: |
| 172 | raise notfound() |
| 173 | fpkg, epath = edat |
| 174 | assert fpkg == epkg |
| 175 | except notfound: |
| 176 | if default is KeyError: |
| 177 | raise KeyError(pkg) |
| 178 | else: |
| 179 | return default |
| 180 | ret = [] |
| 181 | while fpkg == epkg: |
| 182 | ret.append(epath.decode("utf-8")) |
| 183 | edat = cur.next() |
| 184 | if edat is None: |
| 185 | break |
| 186 | fpkg, epath = edat |
| 187 | return ret |
| 188 | |
| 189 | @classmethod |
| 190 | def home(cls): |
| 191 | home = pwd.getpwuid(os.getuid()).pw_dir |
| 192 | return cls(pj(home, "sys"), pj(home, ".tpkg/db")) |
| 193 | |
| 194 | @classmethod |
| 195 | def test(cls): |
| 196 | home = pwd.getpwuid(os.getuid()).pw_dir |
| 197 | return cls(pj(home, "tpkgtest"), pj(home, ".tpkg/testdb")) |
| 198 | |
| 199 | @classmethod |
| 200 | def local(cls): |
| 201 | return cls("/usr/local", "/usr/local/etc/tpkg/db") |
| 202 | |
| 203 | class vfsfile(object): |
| 204 | def __init__(self, path, fullpath): |
| 205 | self.path = path |
| 206 | self.fullpath = fullpath |
| 207 | |
| 208 | def open(self): |
| 209 | return open(self.fullpath, "rb") |
| 210 | |
| 211 | def stat(self): |
| 212 | return os.stat(self.fullpath) |
| 213 | |
| 214 | class vfspkg(object): |
| 215 | def __init__(self, root): |
| 216 | self.root = root |
| 217 | |
| 218 | def __iter__(self): |
| 219 | def scan(lp, fp): |
| 220 | dpre = "" if (lp is "") else lp + "/" |
| 221 | for dent in os.scandir(fp): |
| 222 | dpath = dpre + dent.name |
| 223 | if dent.is_dir(): |
| 224 | yield from scan(dpath, dent.path) |
| 225 | else: |
| 226 | yield vfsfile(dpath, dent.path) |
| 227 | return scan("", self.root) |
| 228 | |
| 229 | def copy(dst, src): |
| 230 | dig = hashlib.sha256() |
| 231 | while True: |
| 232 | buf = src.read(65536) |
| 233 | if buf == b"": |
| 234 | return dig.hexdigest().lower() |
| 235 | dst.write(buf) |
| 236 | dig.update(buf) |
| 237 | |
| 238 | def digest(fp): |
| 239 | dig = hashlib.sha256() |
| 240 | while True: |
| 241 | buf = fp.read(65536) |
| 242 | if buf == b"": |
| 243 | return dig.hexdigest().lower() |
| 244 | dig.update(buf) |
| 245 | |
| 246 | def install(pfx, pkg, pkgname): |
| 247 | for fl in pkg: |
| 248 | if os.path.exists(pj(pfx.root, fl.path)): |
| 249 | sys.stderr.write("tpkg: %s: already exists\n" % (fl.path)) |
| 250 | sys.exit(1) |
| 251 | for fl in pkg: |
| 252 | tp = pj(pfx.root, fl.path) |
| 253 | tpdir = os.path.dirname(tp) |
| 254 | if not os.path.isdir(tpdir): |
| 255 | os.makedirs(tpdir) |
| 256 | tmpp = tp + ".tpkg-new" |
| 257 | sb = fl.stat() |
| 258 | with open(tmpp, "wb") as ofp: |
| 259 | os.fchmod(ofp.fileno(), sb.st_mode & 0o7777) |
| 260 | with fl.open() as ifp: |
| 261 | dig = copy(ofp, ifp) |
| 262 | pfx.regfile(fl.path, pkgname, dig) |
| 263 | os.rename(tmpp, tp) |
| 264 | os.utime(tp, ns=(time.time(), sb.st_mtime)) |
| 265 | |
| 266 | def uninstall(pfx, pkg): |
| 267 | for fn in pfx.pkgfiles(pkg): |
| 268 | fpath = pj(pfx.root, fn) |
| 269 | if not os.path.exists(fpath): |
| 270 | sys.stderr.write("tpkg: warning: %s does not exist\n" % (fn)) |
| 271 | else: |
| 272 | fdat = pfx.filedata(fn) |
| 273 | with open(fpath, "rb") as fp: |
| 274 | if digest(fp) != fdat.get("digest", ""): |
| 275 | sys.stderr.write("tpkg: %s does not match registered hash\n" % (fn)) |
| 276 | sys.exit(1) |
| 277 | for fn in pfx.pkgfiles(pkg): |
| 278 | fpath = pj(pfx.root, fn) |
| 279 | try: |
| 280 | os.unlink(fpath) |
| 281 | except FileNotFoundError: |
| 282 | pass |
| 283 | pfx.unregfile(fn) |
| 284 | |
| 285 | cmds = {} |
| 286 | |
| 287 | def cmd_install(argv): |
| 288 | def usage(out): |
| 289 | out.write("usage: tpkg install [-n NAME] SOURCEDIR\n") |
| 290 | opts, args = getopt.getopt(argv, "n:") |
| 291 | pkgname = None |
| 292 | for o, a in opts: |
| 293 | if o == "-n": |
| 294 | pkgname = a |
| 295 | if len(args) < 1: |
| 296 | usage(sys.stderr) |
| 297 | sys.exit(1) |
| 298 | srcpath = args[0] |
| 299 | if not os.path.isdir(srcpath): |
| 300 | sys.stderr.write("tpkg: %s: not a directory\n" % (srcpath)) |
| 301 | sys.exit(1) |
| 302 | if pkgname is None: |
| 303 | pkgname = os.path.basename(os.path.realpath(srcpath)) |
| 304 | if not pkgname: |
| 305 | sys.stderr.write("tpkg: could not determine package name\n") |
| 306 | sys.exit(1) |
| 307 | install(prefix.use, vfspkg(srcpath), pkgname) |
| 308 | cmds["install"] = cmd_install |
| 309 | |
| 310 | def cmd_uninstall(argv): |
| 311 | def usage(out): |
| 312 | out.write("usage: tpkg uninstall NAME\n") |
| 313 | opts, args = getopt.getopt(argv, "") |
| 314 | if len(args) < 1: |
| 315 | usage(sys.stderr) |
| 316 | sys.exit(1) |
| 317 | pkgname = args[0] |
| 318 | uninstall(prefix.use, pkgname) |
| 319 | cmds["uninstall"] = cmd_uninstall |
| 320 | |
| 321 | def usage(file): |
| 322 | file.write("usage:\ttpkg help\n") |
| 323 | cmds["help"] = lambda argv: usage(sys.stdout) |
| 324 | |
| 325 | def main(argv): |
| 326 | pfx = None |
| 327 | opts, args = getopt.getopt(argv, "hp:") |
| 328 | for o, a in opts: |
| 329 | if o == "-h": |
| 330 | usage(sys.stdout) |
| 331 | sys.exit(0) |
| 332 | elif o == "-p": |
| 333 | if a == "sys": |
| 334 | pfx = prefix.home() |
| 335 | elif a == "local": |
| 336 | pfx = prefix.local() |
| 337 | elif a == "test": |
| 338 | pfx = prefix.test() |
| 339 | else: |
| 340 | sys.stderr.write("tpkg: %s: undefined prefix\n" % (a)) |
| 341 | sys.exit(1) |
| 342 | if pfx is None: |
| 343 | sys.stderr.write("tpkg: no prefix specified\n") |
| 344 | sys.exit(1) |
| 345 | prefix.use = pfx |
| 346 | try: |
| 347 | if len(args) > 0 and args[0] in cmds: |
| 348 | cmds[args[0]](args[1:]) |
| 349 | pfx.maint() |
| 350 | sys.exit(0) |
| 351 | else: |
| 352 | usage(sys.stderr) |
| 353 | sys.exit(1) |
| 354 | finally: |
| 355 | pfx.close() |
| 356 | |
| 357 | if __name__ == "__main__": |
| 358 | main(sys.argv[1:]) |