commit 470c7a0926e72f5eaf1d0d98970341afc2524a31
Author: Antoine Amarilli <a3nm@a3nm.net>
Date: Fri, 28 Dec 2012 17:04:12 +0100
initial commit
Diffstat:
cachefs.py | | | 271 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 271 insertions(+), 0 deletions(-)
diff --git a/cachefs.py b/cachefs.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python
+
+# inspired by
+# https://github.com/terencehonles/fusepy/blob/master/examples/loopback.py
+
+from fuse import Fuse
+import fuse
+import errno
+import logging
+import os
+import shutil
+import sys
+from threading import Lock
+
+class __Struct:
+ def __init__(self, **kw):
+ for k, v in kw.iteritems():
+ setattr(self, k, v)
+
+class Stat(__Struct): pass
+
+class FileTooBigException:
+ pass
+
+fuse.fuse_python_api = (0, 2)
+
+class CacheFS(fuse.Fuse):
+ #def __init__(self, source, cache, db):
+ # self.source = source
+ # self.cache = cache
+ # self.db = db
+ # self.rwlock = Lock()
+
+ def __init__(self, *args, **kw):
+ fuse.Fuse.__init__(self, *args, **kw)
+
+ self.rwlock = Lock()
+
+ # Initialize a Logger() object to handle logging.
+ self.logger = logging.getLogger('cachefs')
+ self.logger.setLevel(logging.INFO)
+ self.logger.addHandler(logging.StreamHandler(sys.stderr))
+ self.parser.add_option('--source', dest='source', metavar='SOURCE',
+ help="source")
+ self.parser.add_option('--cache', dest='cache', metavar='CACHE',
+ help="cache")
+ self.parser.add_option('--db', dest='db', metavar='DB',
+ help="db")
+ self.parser.add_option('--size', dest='size', metavar='SIZE',
+ type='int', help="size")
+
+ def fsinit(self):
+ options = self.cmdline[0]
+ self.source = options.source
+ self.cache = options.cache
+ self.db = options.db
+ self.size = options.size
+
+ def sourcePath(self, path):
+ return os.path.join(self.source, "./"+path)
+ #TODO
+ #return self.source+path
+
+ def cachePath(self, path):
+ #TODO
+ return os.path.join(self.cache, "./"+path)
+ #return self.cache+path
+
+ def statvfsCache(self):
+ return os.statvfs(self.cache)
+
+ def availableCache(self):
+ # TODO account the files, there is no better solution...
+ stv = self.statvfsCache()
+ return stv.f_bsize * stv.f_bavail
+
+ def totalCache(self):
+ stv = self.statvfsCache()
+ return stv.f_frsize * stv.f_blocks
+
+ def nextTarget(self):
+ """return the next cached file to remove"""
+ return None # TODO
+
+ def makeRoom(self, bytes):
+ # TODO maybe don't flush all the cache for a big file
+ if bytes > self.totalCache():
+ raise FileTooBigException()
+ while bytes > self.availableCache():
+ os.unlink(self.nextTarget())
+
+ def registerHit(self, path):
+ """register a hit for path in the cache"""
+
+ def isCached(self, path):
+ """is a path cached?"""
+ if os.path.exists(self.cachePath(path)):
+ statOriginal = os.stat(self.cachePath(path))
+ statCache = os.stat(self.cachePath(path))
+ if statOriginal.st_size == statCache.st_size:
+ # the cache file is good
+ # TODO better checks
+ return True
+ return False
+
+ def prepare(self, path):
+ #if not flags & os.O_RDONLY:
+ # return self.sourcePath(path)
+ self.registerHit(path)
+ print "PREPARATION"
+ if not os.path.exists(self.sourcePath(path)):
+ # no such original file, let the source handle it
+ print "*** original missing"
+ return self.sourcePath(path)
+ if self.isCached(path):
+ print "*** already cached"
+ return self.cachePath(path)
+ statOriginal = os.stat(self.sourcePath(path))
+ # cache the file and then open it
+ print "PREPARATIONca"
+ with self.rwlock:
+ try:
+ print "*** make room"
+ self.makeRoom(statOriginal.st_size)
+ except FileTooBigException:
+ # no room to cache, open the original file
+ return self.sourcePath(path)
+ print ("*** docopy from %s to %s" % (self.sourcePath(path),
+ self.cachePath(path)))
+ shutil.copy2(self.sourcePath(path), self.cachePath(path))
+ return self.cachePath(path)
+
+ def access(self, path, mode):
+ if not os.access(self.sourcePath(path), mode):
+ return -errno.EACCES
+
+ def chmod(self, path, mode):
+ wasCached = self.isCached(path)
+ retval = os.chmod(self.sourcePath(path), mode)
+ if wasCached:
+ os.chmod(self.cachePath(path), mode) # ignore errors
+ return retval
+
+ def chown(self, path, mode):
+ wasCached = self.isCached(path)
+ retval = os.chown(self.sourcePath(path), mode)
+ if wasCached:
+ os.chown(self.cachePath(path), mode) # ignore errors
+ return retval
+
+ def create(self, path, flags, mode):
+ return os.open(self.sourcePath(path), os.O_WRONLY | os.O_CREAT, mode)
+
+ def getattr(self, path, fh=None):
+ st = os.lstat(self.sourcePath(path))
+ return st
+
+ def link(self, target, source):
+ os.link(self.sourcePath(source), self.sourcePath(target))
+
+ def mkdir(self, path, mode):
+ return os.mkdir(self.sourcePath(path), mode)
+
+ def mknod(self, filename):
+ return os.mknod(self.sourcePath(filename))
+
+ def open(self, path, flags):
+ #print("will call open %s %s" % (self.prepare(path), flags))
+ #return os.open(self.prepare(path), flags)
+ return 0
+
+ def read(self, path, size, offset):
+ f = self.prepare(path) # TODO hmm
+ with self.rwlock:
+ fh = os.open(f, os.O_RDONLY)
+ os.lseek(fh, offset, 0)
+ x = os.read(fh, size)
+ os.close(fh)
+ return x
+
+ def readdir(self, path, fh):
+ path = self.sourcePath(path)
+ myIno = os.stat(path).st_ino
+ yield fuse.Direntry('.', ino=myIno)
+ parentIno = os.stat(os.path.join(path, "..")).st_ino
+ yield fuse.Direntry('..', ino=parentIno)
+ for name in os.listdir(path):
+ ino = os.stat(os.path.join(path, name)).st_ino
+ yield fuse.Direntry(name, ino=ino)
+
+ def readlink(self, path):
+ return os.readlink(self.sourcePath(path))
+
+ def release(self, path, fh):
+ return os.close(fh)
+
+ def rename(self, old, new):
+ wasCached = self.isCached(old)
+ retval = os.rename(self.sourcePath(old), self.sourcePath(new))
+ if wasCached:
+ os.rename(self.cachePath(old), self.cachePath(new))
+ return retval
+
+ def rmdir(self, path):
+ return os.rmdir(self.sourcePath(path))
+
+ def statfs(self, path):
+ stv = os.statvfs(self.sourcePath(path))
+ return stv
+
+ def symlink(self, target, source):
+ return os.symlink(self.sourcePath(source), self.sourcePath(target))
+
+ def doTruncate(self, path, length):
+ with open(path, 'r+') as f:
+ f.truncate(length)
+
+ def truncate(self, path, length, fh=None):
+ wasCached = self.isCached(path)
+ self.doTruncate(self.sourcePath(path), length)
+ if wasCached:
+ self.doTruncate(self.cachePath(path), length)
+
+ def unlink(self, path):
+ wasCached = self.isCached(path)
+ retval = os.unlink(self.sourcePath(path))
+ if wasCached:
+ os.unlink(self.cachePath(path))
+ return retval
+
+ def utimens(self, path, ts_acc, ts_mod):
+ wasCached = self.isCached(path)
+ times = (ts_acc.tv_sec, ts_mod.tv_sec)
+ retval = os.utime(self.sourcePath(path), times)
+ if wasCached:
+ os.utime(self.cachePath(path), times)
+ return retval
+
+ def doWrite(self, path, data, offset):
+ with self.rwlock:
+ fh = os.open(path, os.O_WRONLY)
+ os.lseek(fh, offset, 0)
+ x = os.write(fh, data)
+ os.close(fh)
+ return x
+
+ def write(self, path, data, offset):
+ wasCached = self.isCached(path)
+ print "writing to a %s file" % ("cached" if wasCached else "notcached")
+ retval = self.doWrite(self.sourcePath(path), data, offset)
+ if retval > 0 and wasCached:
+ self.doWrite(self.cachePath(path), data, offset)
+ return retval
+
+
+
+
+
+
+
+if __name__ == "__main__":
+ #if len(sys.argv) != 6:
+ # print("Usage: %s SOURCE CACHE SIZE DB MOUNTPOINT" % sys.argv[0])
+ # sys.exit(1)
+ #logging.getLogger().setLevel(logging.DEBUG)
+ #cachefs = CacheFS(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
+
+ cachefs = CacheFS()
+ fuse_opts = cachefs.parse(['-o', 'fsname=cachefs'] + sys.argv[1:])
+ cachefs.main()
+