summaryrefslogtreecommitdiffstats
path: root/proof-of-concept
diff options
context:
space:
mode:
authorWill Woods <wwoods@redhat.com>2009-01-26 18:23:37 -0500
committerWill Woods <wwoods@redhat.com>2009-01-26 18:23:37 -0500
commit40efb49a96dcb028e921defe93e6d9c84b9f67ca (patch)
tree5146176fcacb4724884e53583ad21b490af7eb70 /proof-of-concept
parent06b40afc07f6d99c0f1dbc9a99ef00be7f4d76ef (diff)
downloaddebuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.tar.gz
debuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.tar.xz
debuginfofs-40efb49a96dcb028e921defe93e6d9c84b9f67ca.zip
Add proof-of-concept FUSE implementation
Diffstat (limited to 'proof-of-concept')
-rwxr-xr-xproof-of-concept/repofs.py236
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()