Last active
March 16, 2022 20:23
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Last single mode version is https://gist.github.com/4547295/987b453cbcc37710959abdb392558201ec3e44e8