Initial commit.
[tpkg.git] / tpkg
CommitLineData
82bfc891
FT
1#!/usr/bin/python3
2
3import sys, os, bsddb3, struct, getopt, pwd, time, hashlib
4bd = bsddb3.db
5pj = os.path.join
6deadlock = bd.DBLockDeadlockError
7notfound = bd.DBNotFoundError
8
9class 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
31class 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
42def 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
59class 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
203class 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
214class 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
229def 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
238def 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
246def 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
266def 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
285cmds = {}
286
287def 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)
308cmds["install"] = cmd_install
309
310def 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)
319cmds["uninstall"] = cmd_uninstall
320
321def usage(file):
322 file.write("usage:\ttpkg help\n")
323cmds["help"] = lambda argv: usage(sys.stdout)
324
325def 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
357if __name__ == "__main__":
358 main(sys.argv[1:])