diff options
author | Will Woods <wwoods@redhat.com> | 2009-01-26 18:23:37 -0500 |
---|---|---|
committer | Will Woods <wwoods@redhat.com> | 2009-01-26 18:23:37 -0500 |
commit | 40efb49a96dcb028e921defe93e6d9c84b9f67ca (patch) | |
tree | 5146176fcacb4724884e53583ad21b490af7eb70 /proof-of-concept | |
parent | 06b40afc07f6d99c0f1dbc9a99ef00be7f4d76ef (diff) | |
download | debuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.tar.gz debuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.tar.xz debuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.zip |
Add proof-of-concept FUSE implementation
Diffstat (limited to 'proof-of-concept')
-rwxr-xr-x | proof-of-concept/repofs.py | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/proof-of-concept/repofs.py b/proof-of-concept/repofs.py new file mode 100755 index 0000000..d3ad279 --- /dev/null +++ b/proof-of-concept/repofs.py @@ -0,0 +1,236 @@ +#!/usr/bin/python + +import os +import glob +from stat import * +import errno +import subprocess +import time + +import fuse +fuse.fuse_python_api = (0, 2) +fuse.feature_assert('stateful_files', 'has_init') +from fuse import Fuse + +from sqlite3 import dbapi2 as sqlite + +import bz2, gzip + +import yum.repoMDObject + +def bunzip(infile,outfile): + (p,f) = os.path.split(outfile) + if not os.path.isdir(p): + os.makedirs(p) + outf = open(outfile,"w") + inf = bz2.BZ2File(infile) + data = inf.read(4096) + while data: + outf.write(data) + data = inf.read(4096) + inf.close() + outf.close() + +def flag2mode(flags): + md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} + m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] + if flags | os.O_APPEND: + m = m.replace('w', 'a', 1) + return m + +def fuseclonestat(stat): + s = fuse.Stat() + (s.st_mode, s.st_ino, s.st_dev, s.st_nlink, s.st_uid, s.st_gid, s.st_size, + s.st_atime, s.st_mtime, s.st_ctime) = stat + return s + +class SimpleYumRepo(object): + def __init__(self, path=None, cachedir=None): + self.path = path + self.cachedir = cachedir + self._attrcache = {} + self._linkcache = {} + if path: + self.parse_repomd() + + def parse_repomd(self): + repomd = os.path.join(self.path, "repodata/repomd.xml") + repoXML = yum.repoMDObject.RepoMD(self.path, repomd) + for t in ('primary_db', 'filelists_db'): + if t in repoXML.fileTypes(): + d = repoXML.getData(t) + (base,dbpath) = d.location + dbfile = os.path.join(self.path,dbpath) + # TODO check for existing db file + if dbfile.endswith(".bz2"): + outfile = os.path.join(self.cachedir,".repodata", + os.path.basename(dbfile)[:-4]) + bunzip(dbfile,outfile) + dbfile = outfile + # TODO: elif .gz, else... + con = sqlite.connect(dbfile) + setattr(self,t,con) + + def packages_for_file(self, path): + '''Return a (possibly-empty) list of RPMs containing the file with the + given path.''' + keys = [] + (dirname,filename) = os.path.split(path) + c = self.filelists_db.cursor() + c.execute("SELECT pkgKey,filenames FROM filelist " + "WHERE dirname=?",(dirname,)) + keys = [k for (k, f) in c if filename in f.split('/')] + c.close() + rpms = [] + for key in keys: + # FIXME do one query? WHERE pkgKey=? OR ... + # Nope, too long, but maybe we could do a join.. + c = self.primary_db.cursor() + c.execute("SELECT location_base,location_href FROM packages " + "WHERE pkgKey=?",(key,)) + (base,href) = c.fetchone() + rpms.append(os.path.join(self.path,href)) + c.close() + return rpms + + def unpack(self,rpm,targetdir=None): + if targetdir: + if not os.path.isdir(targetdir): + os.makedirs(targetdir) + else: + targetdir = self.cachedir + + inf = open(rpm) + # Find RPM header and read compression algorithm + # Skip forward to gzipped CPIO archive + # FIXME: Awful. Just awful. + header = inf.read(409600) + offset = header.index("\x1f\x8b") + del header + inf.seek(offset) + gz = gzip.GzipFile(fileobj=inf, mode="rb") + # Open a pipe to "cpio -iumd --quiet" + cpio = subprocess.Popen(args=["cpio","-iumd","--quiet"], + cwd=targetdir, stdin=subprocess.PIPE) + data = gz.read(4096) + while data: + cpio.stdin.write(data) + data = gz.read(4096) + gz.close() + inf.close() + cpio.stdin.close() + cpio.wait() + +class Repofs(Fuse): + def __init__(self, *args, **kw): + Fuse.__init__(self, *args, **kw) + + def fsinit(self): + if not os.path.isdir(self.cachedir): + os.makedirs(self.cachedir) + self.logfile = open(os.path.join(self.cachedir,".log"),"a",0) + self.log("fsinit(path=%s). Hang on.." % self.repopath) + # TODO: figure out the repo type (Yum, etc) and use the right class + self.repos = [] + for rp in self.repopath.split(":"): + r = SimpleYumRepo(path=rp, cachedir=self.cachedir) + self.repos.append(r) + self.log(" cachedir=%s, repopath=%s" % (r.cachedir, r.path)) + + def log(self,message): + self.logfile.write("%s %s\n" % (time.asctime(), message)) + + def _package_for_file(self, path): + for repo in self.repos: + packages = repo.packages_for_file(path) + if packages: + return (packages[0], repo) + + def _cachefile(self, path): + '''Find the given filename in the repo, extract it from whatever + package it's in, and save it into the cache. + Returns the actual on-disk path for the (now cached) file.''' + cachefile = os.path.join(self.cachedir, path.strip('/')) + if os.path.exists(cachefile): + return cachefile + # FIXME try/except + # FIXME multiple repos + # FIXME assumes that filenames are unique + (package, repo) = self._package_for_file(path) + if not package: + return None + repo.unpack(package, self.cachedir) + self.log("unpacked %s" % package) + if os.path.exists(cachefile): + return cachefile + + def getattr(self, path): + self.log("getattr(%s)" % path) + f = self._cachefile(path) + if not f: + return -errno.ENOENT + # Not os.lstat() - we're ignoring symlinks. + # This means we don't have to implement readlink(). + attr = fuseclonestat(os.stat(f)) + # No writeable files - turn off write bits + attr.st_mode &= ~(S_IWUSR|S_IWGRP|S_IWOTH) + return attr + + # FIXME actually use the provided arg + def utime(self, path, times): + self.log("utime(%s, %s)" % path, str(times)) + return os.utime(self._cachefile(path)) + def access(self, path, mode): + self.log("access(%s, %s)" % path, mode) + if not os.access(self._cachefile(path)): + return -errno.EACCES + + def readdir(self, path, offset): + '''Return an empty list. We don't let you list directory contents.''' + self.log("Huh? readdir(%s)" % path) + return [] + + def statfs(self): + #self.log("statfs()") + s = fuse.StatVFS() + local_s = os.statvfs(self.cachedir) + # FIXME modify s using info from local_s + return local_s + + # XXX explicitly declare other functions that return proper error codes? + + def open(self, path, flags): + self.log("open(%s,%s)" % (path,flags)) + return open(self._cachefile(path),flag2mode(flags)) + + def read(self, path, length, offset, fh=None): + self.log("read(%s,%s,%s)" % (path,length,offset)) + fh.seek(offset) + return fh.read(length) + + def release(self, path, fh=None): + self.log("release(%s)" % path) + fh.close() + + def fgetattr(self, path, fh=None): + self.log("fgetattr(%s)" % path) + return os.fstat(fh.fileno()) + + def main(self, *a, **kw): + return Fuse.main(self, *a, **kw) + + # FIXME need some kind of destroy() that removes the per-instance cache + +def main(): + usage = 'Repofs: mount a package repo and export all the files in the packages.\n\n' + Fuse.fusage + server = Repofs(version="%prog " + fuse.__version__, usage=usage, + dash_s_do='setsingle') + server.parser.add_option(mountopt="repo", metavar="PATH", dest="repopath", + help="Package repo to mount") + server.parser.add_option(mountopt="cachedir", metavar="PATH", + help="Cache dir for expanded packages") + server.parse(values=server, errex=1) + server.main() + +if __name__ == '__main__': + main() |