Skip to content

Instantly share code, notes, and snippets.

@Vayu
Last active March 16, 2022 20:23
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Vayu/4547295 to your computer and use it in GitHub Desktop.
Save Vayu/4547295 to your computer and use it in GitHub Desktop.
Obnam (http://liw.fi/obnam/) plugin to mount a backup generation as FUSE filesystem
# Copyright (C) 2013 Valery Yundin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import stat
import sys
import logging
import errno
import obnamlib
try:
import fuse
fuse.fuse_python_api = (0, 2)
except ImportError:
class Bunch:
def __init__(self, **kwds):
self.__dict__.update(kwds)
fuse = Bunch(Fuse = object)
class ObnamFSOptParse(object):
'''Option parsing class for FUSE
has to set fuse_args.mountpoint
'''
obnam = None
def __init__(self, *args, **kw):
self.fuse_args = \
'fuse_args' in kw and kw.pop('fuse_args') or fuse.FuseArgs()
if 'fuse' in kw:
self.fuse = kw.pop('fuse')
def parse_args(self, args=None, values=None):
self.fuse_args.mountpoint = self.obnam.app.settings['to']
class ObnamFS(fuse.Fuse):
'''FUSE main class
'''
MAX_METADATA_CACHE = 1000
def get_metadata(self, path):
try:
return self.metadatacache[path]
except KeyError:
if len(self.metadatacache) > self.MAX_METADATA_CACHE:
self.metadatacache.clear()
metadata = self.obnam.repo.get_metadata(self.obnam.gen,
self.obnampath(path))
self.metadatacache[path] = metadata
return metadata
def init_mount(self):
mountroot = self.obnam.mountroot
if mountroot == '/':
self.obnampath = lambda path : path
else:
self.obnampath = lambda path : path == '/' and mountroot or mountroot + path
def __init__(self, *args, **kw):
self.obnam = kw['obnam']
self.metadatacache = {}
self.openedfiles = {}
self.readcache = {}
self.init_mount()
fuse.Fuse.__init__(self, *args, **kw)
def getattr(self, path):
try:
metadata = self.get_metadata(path)
st = fuse.Stat()
st.st_mode = metadata.st_mode
st.st_dev = metadata.st_dev
st.st_nlink = metadata.st_nlink
st.st_uid = metadata.st_uid
st.st_gid = metadata.st_gid
st.st_size = metadata.st_size
st.st_atime = metadata.st_atime_sec
st.st_mtime = metadata.st_mtime_sec
st.st_ctime = st.st_mtime
return st
except obnamlib.Error:
return -errno.ENOENT
except:
logging.error('Unexpected exception', exc_info=True)
raise
def readdir(self, path, fh):
logging.debug('fuse readdir(%s, %s)', path, repr(fh))
try:
listdir = ['.','..'] + self.obnam.repo.listdir(self.obnam.gen,
self.obnampath(path))
return [fuse.Direntry(name) for name in listdir]
except obnamlib.Error:
return -errno.EINVAL
except:
logging.error('Unexpected exception', exc_info=True)
raise
def open(self, path, flags):
logging.debug('fuse open(%s, %s)', path, repr(flags))
if ((flags & os.O_WRONLY) or (flags & os.O_RDWR) or
(flags & os.O_CREAT) or (flags & os.O_EXCL) or
(flags & os.O_TRUNC) or (flags & os.O_APPEND)):
return -errno.EROFS
else:
if path not in self.openedfiles:
self.openedfiles[path] = {'chunksizes' : []}
return 0
def create(self, path, flags):
return -errno.EROFS
def read(self, path, length, offset):
logging.debug('fuse read(%s, %d, %d)', path, length, offset)
try:
metadata = self.get_metadata(path)
# if not a regular file return EINVAL
if not stat.S_ISREG(metadata.st_mode):
return -errno.EINVAL
# if stored inside B-tree
contents = self.obnam.repo.get_file_data(self.obnam.gen,
self.obnampath(path))
if contents is not None:
return contents[offset:offset+length]
# stored in chunks
data = ''
begin = 0
end = 0
chunksizes = self.openedfiles[path]['chunksizes']
chunkids = []
nextidx = 0
for chunklen in chunksizes:
end += chunklen
nextidx += 1
if end > offset:
break
if (self.readcache
and self.readcache['index'] == nextidx - 1
and self.readcache['path'] == path):
idx = nextidx = nextidx - 1
chunklen = chunksizes[idx]
begin = end - chunklen
data = self.readcache['data']
chunkids = self.readcache['chunkids']
elif end > offset:
nextidx -= 1
idx = nextidx - 1
end -= chunklen
begin = end
else:
idx = nextidx - 1
begin = end
if not chunkids:
chunkids = self.obnam.repo.get_file_chunks(self.obnam.gen,
self.obnampath(path))
self.readcache.clear()
output = []
nbytes = 0
while nbytes < length and nextidx < len(chunkids):
if idx != nextidx:
idx = nextidx
data = self.obnam.repo.get_chunk(chunkids[idx])
chunklen = len(data)
end += chunklen
if idx == len(chunksizes):
chunksizes.append(chunklen)
if begin >= offset:
cut_to = offset + length - begin
if cut_to > chunklen:
cut_to = chunklen
output.append(data[:cut_to])
nbytes += cut_to
elif end > offset:
cut_from = offset-begin
cut_to = offset + length - begin
if cut_to > chunklen:
cut_to = chunklen
output.append(data[cut_from:cut_to])
nbytes += cut_to - cut_from
begin = end
nextidx += 1
self.readcache['path'] = path
self.readcache['chunkids'] = chunkids
self.readcache['index'] = idx
self.readcache['data'] = data
self.openedfiles[path]['chunksizes'] = chunksizes
return ''.join(output)
except obnamlib.Error:
return -errno.ENOENT
except:
logging.error('Unexpected exception', exc_info=True)
raise
def readlink(self, path):
try:
metadata = self.get_metadata(path)
if metadata.islink():
return metadata.target
else:
return -errno.EINVAL
except obnamlib.Error:
return -errno.ENOENT
except:
logging.error('Unexpected exception', exc_info=True)
raise
def release(self, path, flags):
if path in self.openedfiles:
del self.openedfiles[path]
return 0
def statfs(self):
logging.debug('fuse statfs')
stv = fuse.StatVfs()
stv.f_bsize = 65536
stv.f_frsize = 0
stv.f_blocks = self.obnam.repo.client.get_generation_data(self.obnam.gen)/65536
stv.f_bfree = 0
stv.f_bavail = 0
stv.f_files = self.obnam.repo.client.get_generation_file_count(self.obnam.gen)
stv.f_ffree = 0
stv.f_favail = 0
stv.f_flag = 0
stv.f_namemax = 255
#return -errno.ENOSYS
return stv
def fsync(self, path, isFsyncFile):
return 0
def chmod(self, path, mode):
return -errno.EROFS
def chown(self, path, uid, gid):
return -errno.EROFS
def link(self, targetPath, linkPath):
return -errno.EROFS
def mkdir(self, path, mode):
return -errno.EROFS
def mknod(self, path, mode, dev):
return -errno.EROFS
def rename(self, oldPath, newPath):
return -errno.EROFS
def rmdir(self, path):
return -errno.EROFS
def symlink(self, targetPath, linkPath):
return -errno.EROFS
def truncate(self, path, size):
return -errno.EROFS
def unlink(self, path):
return -errno.EROFS
def utime(self, path, times):
return -errno.EROFS
def write(self, path, buf, offset):
return -errno.EROFS
class MountPlugin(obnamlib.ObnamPlugin):
'''Mount backup repository as a user-space filesystem.
At the momemnt only a specific generation can be mounted
'''
def enable(self):
self.app.add_subcommand('mount', self.mount,
arg_synopsis='[ROOT]')
def mount(self, args):
'''Mount a generation as a FUSE filesystem.'''
if not hasattr(fuse, 'fuse_python_api'):
raise obnamlib.Error('Failed to load module "fuse", '
'try installing python-fuse')
self.app.settings.require('repository')
self.app.settings.require('client-name')
self.app.settings.require('generation')
self.app.settings.require('to')
self.repo = self.app.open_repository()
self.repo.open_client(self.app.settings['client-name'])
self.gen = self.repo.genspec(self.app.settings['generation'])
self.mountroot = (['/'] + self.app.settings['root'] + args)[-1]
if self.mountroot != '/':
self.mountroot = self.mountroot.rstrip('/')
self.repo.get_metadata(self.gen, self.mountroot)
logging.debug("Mounting %s@%s:%s to %s", self.app.settings['client-name'],
str(self.gen), self.mountroot, self.app.settings['to'])
try:
ObnamFSOptParse.obnam = self
fs = ObnamFS(obnam=self, parser_class=ObnamFSOptParse)
fs.flags = 0
fs.multithreaded = 0
fs.parse()
fs.main()
except fuse.FuseError, e:
raise obnamlib.Error(repr(e))
self.repo.fs.close()
@Vayu
Copy link
Author

Vayu commented Jan 19, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment