Commit | Line | Data |
---|---|---|
82bfc891 FT |
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): | |
d414a6a0 | 72 | sys.stderr.write("tpkg: creating %s...\n" % (self.envdir)) |
82bfc891 FT |
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): | |
56c42591 | 220 | dpre = "" if (lp == "") else lp + "/" |
82bfc891 FT |
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() | |
93be9605 FT |
258 | try: |
259 | with open(tmpp, "wb") as ofp: | |
260 | os.fchmod(ofp.fileno(), sb.st_mode & 0o7777) | |
261 | with fl.open() as ifp: | |
262 | dig = copy(ofp, ifp) | |
263 | pfx.regfile(fl.path, pkgname, dig) | |
264 | except: | |
265 | try: | |
266 | os.unlink(tmpp) | |
267 | except FileNotFoundError: | |
268 | pass | |
269 | raise | |
82bfc891 | 270 | os.rename(tmpp, tp) |
5f29f621 | 271 | os.utime(tp, ns=(round(time.time() * 1e9), round(sb.st_mtime * 1e9))) |
82bfc891 FT |
272 | |
273 | def uninstall(pfx, pkg): | |
274 | for fn in pfx.pkgfiles(pkg): | |
275 | fpath = pj(pfx.root, fn) | |
276 | if not os.path.exists(fpath): | |
277 | sys.stderr.write("tpkg: warning: %s does not exist\n" % (fn)) | |
278 | else: | |
279 | fdat = pfx.filedata(fn) | |
280 | with open(fpath, "rb") as fp: | |
281 | if digest(fp) != fdat.get("digest", ""): | |
282 | sys.stderr.write("tpkg: %s does not match registered hash\n" % (fn)) | |
283 | sys.exit(1) | |
284 | for fn in pfx.pkgfiles(pkg): | |
285 | fpath = pj(pfx.root, fn) | |
286 | try: | |
287 | os.unlink(fpath) | |
288 | except FileNotFoundError: | |
289 | pass | |
290 | pfx.unregfile(fn) | |
291 | ||
292 | cmds = {} | |
293 | ||
294 | def cmd_install(argv): | |
295 | def usage(out): | |
296 | out.write("usage: tpkg install [-n NAME] SOURCEDIR\n") | |
297 | opts, args = getopt.getopt(argv, "n:") | |
298 | pkgname = None | |
299 | for o, a in opts: | |
300 | if o == "-n": | |
301 | pkgname = a | |
302 | if len(args) < 1: | |
303 | usage(sys.stderr) | |
304 | sys.exit(1) | |
305 | srcpath = args[0] | |
306 | if not os.path.isdir(srcpath): | |
307 | sys.stderr.write("tpkg: %s: not a directory\n" % (srcpath)) | |
308 | sys.exit(1) | |
309 | if pkgname is None: | |
310 | pkgname = os.path.basename(os.path.realpath(srcpath)) | |
311 | if not pkgname: | |
312 | sys.stderr.write("tpkg: could not determine package name\n") | |
313 | sys.exit(1) | |
314 | install(prefix.use, vfspkg(srcpath), pkgname) | |
315 | cmds["install"] = cmd_install | |
316 | ||
317 | def cmd_uninstall(argv): | |
318 | def usage(out): | |
319 | out.write("usage: tpkg uninstall NAME\n") | |
320 | opts, args = getopt.getopt(argv, "") | |
321 | if len(args) < 1: | |
322 | usage(sys.stderr) | |
323 | sys.exit(1) | |
324 | pkgname = args[0] | |
325 | uninstall(prefix.use, pkgname) | |
326 | cmds["uninstall"] = cmd_uninstall | |
327 | ||
21599053 FT |
328 | def cmd_list(argv): |
329 | def usage(out): | |
330 | out.write("usage: tpkg list NAME\n") | |
331 | opts, args = getopt.getopt(argv, "") | |
332 | if len(args) < 1: | |
333 | usage(sys.stderr) | |
334 | sys.exit(1) | |
335 | pkgname = args[0] | |
336 | try: | |
337 | files = prefix.use.pkgfiles(pkgname) | |
338 | except KeyError: | |
339 | sys.stderr.write("tpkg: %s: no such package\n" % (pkgname)) | |
340 | sys.exit(1) | |
341 | for fn in files: | |
342 | sys.stdout.write("%s\n" % pj(prefix.use.root, fn)) | |
343 | cmds["list"] = cmd_list | |
344 | ||
82bfc891 FT |
345 | def usage(file): |
346 | file.write("usage:\ttpkg help\n") | |
347 | cmds["help"] = lambda argv: usage(sys.stdout) | |
348 | ||
349 | def main(argv): | |
350 | pfx = None | |
351 | opts, args = getopt.getopt(argv, "hp:") | |
352 | for o, a in opts: | |
353 | if o == "-h": | |
354 | usage(sys.stdout) | |
355 | sys.exit(0) | |
356 | elif o == "-p": | |
357 | if a == "sys": | |
358 | pfx = prefix.home() | |
359 | elif a == "local": | |
360 | pfx = prefix.local() | |
361 | elif a == "test": | |
362 | pfx = prefix.test() | |
363 | else: | |
364 | sys.stderr.write("tpkg: %s: undefined prefix\n" % (a)) | |
365 | sys.exit(1) | |
366 | if pfx is None: | |
367 | sys.stderr.write("tpkg: no prefix specified\n") | |
368 | sys.exit(1) | |
369 | prefix.use = pfx | |
370 | try: | |
371 | if len(args) > 0 and args[0] in cmds: | |
372 | cmds[args[0]](args[1:]) | |
373 | pfx.maint() | |
374 | sys.exit(0) | |
375 | else: | |
376 | usage(sys.stderr) | |
377 | sys.exit(1) | |
378 | finally: | |
379 | pfx.close() | |
380 | ||
381 | if __name__ == "__main__": | |
382 | main(sys.argv[1:]) |