gpgfs/gpgfs.py

409 lines
13 KiB
Python
Raw Normal View History

2014-04-27 22:43:14 +02:00
#!/usr/bin/env python
2014-05-01 19:32:30 +02:00
from fuse import FUSE, FuseOSError, Operations
2014-05-01 18:59:51 +02:00
import errno
2014-04-27 22:43:14 +02:00
import stat
import os
import sys
import logging
import struct
import time
2015-12-08 00:15:47 +01:00
from io import BytesIO
import gpgstore
from contextlib import contextmanager
from threading import Lock
2014-04-27 22:43:14 +02:00
2015-12-08 00:15:47 +01:00
magic = b'GPGFS1\n'
2014-04-27 22:43:14 +02:00
log = logging.getLogger('gpgfs')
2014-04-27 22:43:14 +02:00
class Entry:
'''
Filesystem object, either file or directory.
'''
def __init__(self, **kwargs):
2015-12-08 00:15:47 +01:00
for k,v in kwargs.items():
2014-04-27 22:43:14 +02:00
setattr(self, k, v)
def read_index(store, path):
if not store.exists(path):
now = time.time()
root = Entry(children={}, nlink=3, size=0,
2015-12-08 00:15:47 +01:00
mode=stat.S_IFDIR | 0o755,
mtime=now, ctime=now)
write_index(store, path, root)
log.info('created %s', path)
return root
2014-05-18 11:34:46 +02:00
data = store.get(path, format=gpgstore.FMT_GPG)
2015-12-08 00:15:47 +01:00
buf = BytesIO(data)
temp = buf.read(len(magic))
if temp != magic:
raise IOError('index parse error: %s' % path)
2014-04-27 22:43:14 +02:00
read_atom(buf)
root = Entry(**read_dict(buf))
return root
def write_index(store, path, root):
2015-12-08 00:15:47 +01:00
buf = BytesIO()
2014-04-27 22:43:14 +02:00
buf.write(magic)
2015-12-08 00:15:47 +01:00
header = b''
2014-04-27 22:43:14 +02:00
write_atom(buf, header)
write_dict(buf, root)
2014-05-18 11:34:46 +02:00
store.put(buf.getvalue(), path=path, format=gpgstore.FMT_GPG)
2014-04-27 22:43:14 +02:00
def write_dict(fd, dct):
# breadth-first
children = []
2015-12-08 00:15:47 +01:00
buf = BytesIO()
2014-04-27 22:43:14 +02:00
if not isinstance(dct, dict):
dct = dct.__dict__
for key in dct:
2015-12-08 00:15:47 +01:00
write_atom(buf, key.encode('utf-8'))
2014-04-27 22:43:14 +02:00
val = dct[key]
if isinstance(val, dict):
2015-12-08 00:15:47 +01:00
buf.write(b'D')
2014-04-27 22:43:14 +02:00
children.append(val)
elif isinstance(val, Entry):
2015-12-08 00:15:47 +01:00
buf.write(b'E')
2014-04-27 22:43:14 +02:00
children.append(val)
2015-12-08 00:15:47 +01:00
elif isinstance(val, (int)):
if val < 2**32:
2015-12-08 00:15:47 +01:00
buf.write(b'I')
buf.write(struct.pack('<I', val))
else:
2015-12-08 00:15:47 +01:00
buf.write(b'L')
buf.write(struct.pack('<Q', val))
elif isinstance(val, float):
2015-12-08 00:15:47 +01:00
buf.write(b'F')
buf.write(struct.pack('<d', val))
2015-12-08 00:15:47 +01:00
elif isinstance(val, bytes):
buf.write(b'')
2014-04-27 22:43:14 +02:00
write_atom(buf, val)
2015-12-08 00:15:47 +01:00
elif isinstance(val, str):
buf.write(b'S')
write_atom(buf, val.encode('utf-8'))
2014-04-27 22:43:14 +02:00
else:
2015-12-08 00:15:47 +01:00
raise TypeError(type(val))
2014-04-27 22:43:14 +02:00
write_atom(fd, buf.getvalue())
for c in children:
write_dict(fd, c)
def read_dict(fd):
dct = {}
buf = read_atom(fd)
buflen = len(buf)
2015-12-08 00:15:47 +01:00
buf = BytesIO(buf)
2014-04-27 22:43:14 +02:00
while buf.tell() < buflen:
2015-12-08 00:15:47 +01:00
key = read_atom(buf).decode('utf-8')
2014-04-27 22:43:14 +02:00
tag = buf.read(1)
2015-12-08 00:15:47 +01:00
if tag == b'D': val = read_dict(fd)
elif tag == b'E': val = Entry(**read_dict(fd))
elif tag == b'I': val = struct.unpack('<I', buf.read(4))[0]
elif tag == b'L': val = struct.unpack('<Q', buf.read(8))[0]
elif tag == b'F': val = struct.unpack('<d', buf.read(8))[0]
elif tag == b'': val = read_atom(buf)
elif tag == b'S': val = read_atom(buf).decode('utf-8')
else: raise TypeError(tag)
2014-04-27 22:43:14 +02:00
dct[key] = val
return dct
def write_atom(fd, atom):
2015-12-08 00:15:47 +01:00
assert isinstance(atom, bytes)
2014-04-27 22:43:14 +02:00
fd.write(struct.pack('<I', len(atom)))
fd.write(atom)
def read_atom(fd):
return fd.read(struct.unpack('<I', fd.read(4))[0])
class LoggingMixIn:
def __call__(self, op, path, *args):
if op=='write':
atxt = ' '.join([repr(args[0])[:10], repr(args[1]), repr(args[2])])
else:
atxt = ' '.join(map(repr, args))
2014-04-29 21:14:42 +02:00
log.debug('-> %s %s %s', op, repr(path), atxt)
2014-04-27 22:43:14 +02:00
ret = '[Unhandled Exception]'
try:
ret = getattr(self, op)(path, *args)
return ret
2015-12-08 00:15:47 +01:00
except OSError as e:
2014-04-27 22:43:14 +02:00
ret = str(e)
raise
except:
log.exception('unhandled error in %s:', op)
raise
2014-04-27 22:43:14 +02:00
finally:
rtxt = repr(ret)
if op=='read':
rtxt = rtxt[:10]
2014-04-29 21:14:42 +02:00
log.debug('<- %s %s', op, rtxt)
2014-04-27 22:43:14 +02:00
2014-05-01 19:32:30 +02:00
class GpgFs(LoggingMixIn, Operations):
#class GpgFs(Operations):
2014-04-27 22:43:14 +02:00
def __init__(self, encroot, keyid):
'''
:param encroot: Encrypted root directory
'''
2014-05-01 19:32:30 +02:00
self.encroot = encroot.rstrip('/')
assert os.path.exists(self.encroot)
assert os.path.isdir(self.encroot)
2014-04-27 22:43:14 +02:00
#self.cache = cache
self.store = gpgstore.GpgStore(self.encroot, keyid)
self.index_path = 'index'
self.root = read_index(self.store, self.index_path)
self.txlock = Lock()
2014-04-27 22:43:14 +02:00
self.fd = 0
self._clear_write_cache()
2014-04-27 22:43:14 +02:00
def _find(self, path, parent=False):
assert path.startswith('/')
if path == '/':
return self.root
node = self.root
path = path[1:].split('/')
if parent:
2014-05-01 19:32:30 +02:00
basename = path[-1]
2014-04-27 22:43:14 +02:00
path = path[:-1]
for name in path:
2014-05-01 19:32:30 +02:00
if name not in node.children:
raise FuseOSError(errno.ENOENT)
2014-04-27 22:43:14 +02:00
node = node.children[name]
2014-05-01 19:32:30 +02:00
if parent:
return node, basename
2014-04-27 22:43:14 +02:00
return node
def _clear_write_cache(self):
self.write_path = None
self.write_buf = []
self.write_len = 0
self.write_dirty = False
@contextmanager
def transaction(self):
paths = {'old': None, 'new': None}
def putx(data, old_path = None):
paths['new'] = self.store.put(data)
paths['old'] = old_path
return paths['new']
with self.txlock:
try:
yield putx
# commit
write_index(self.store, self.index_path, self.root)
except:
# rollback
try:
log.warning('starting rollback')
self.root = read_index(self.store, self.index_path)
if paths['new']:
self.store.delete(paths['new'])
log.warning('rollback done')
except:
log.exception('rollback failed')
raise
if paths['old']:
self.store.delete(paths['old'])
def chmod(self, path, mode):
# sanitize mode (clear setuid/gid/sticky bits)
2015-12-08 00:15:47 +01:00
mode &= 0o777
with self.transaction():
ent = self._find(path)
2015-12-08 00:15:47 +01:00
ent.mode = mode | (ent.mode & 0o170000)
2014-04-27 22:43:14 +02:00
def chown(self, path, uid, gid):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def create(self, path, mode):
2015-12-08 00:15:47 +01:00
mode &= 0o777
mode |= stat.S_IFREG
with self.transaction() as putx:
parent, name = self._find(path, parent=True)
if name in parent.children:
raise FuseOSError(errno.EEXIST)
now = time.time()
encpath = putx('')
parent.children[name] = Entry(mode=mode, encpath=encpath, size=0,
2014-05-18 11:34:46 +02:00
nlink=1, ctime=now, mtime=now,
encformat=gpgstore.FMT_GPG)
parent.mtime = now
log.debug('new path %s => %s', path, encpath)
self.fd += 1
return self.fd
2014-04-27 22:43:14 +02:00
def flush(self, path, fh):
if not self.write_dirty:
log.debug('nothing to flush')
2014-04-27 22:43:14 +02:00
return 0
with self.transaction() as putx:
2015-12-08 00:15:47 +01:00
buf = b''.join(self.write_buf)
self.write_buf = [buf]
ent = self._find(self.write_path)
ent.size = len(buf)
ent.encpath = putx(buf, ent.encpath)
self.write_dirty = False
log.debug('flushed %d bytes to %s', len(buf), self.write_path)
return 0
2014-04-27 22:43:14 +02:00
def fsync(self, path, datasync, fh):
self.flush(path, fh)
return 0
2014-04-27 22:43:14 +02:00
def getattr(self, path, fh = None):
# don't do full blown transaction
with self.txlock:
ent = self._find(path)
return dict(st_mode = ent.mode, st_size = ent.size,
st_ctime = ent.ctime, st_mtime = ent.mtime,
st_atime = 0, st_nlink = ent.nlink)
2014-04-27 22:43:14 +02:00
def getxattr(self, path, name, position = 0):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENODATA) # ENOATTR
2014-04-27 22:43:14 +02:00
def listxattr(self, path):
return []
def mkdir(self, path, mode):
2015-12-08 00:15:47 +01:00
mode &= 0o777
mode |= stat.S_IFDIR
with self.transaction():
parent, name = self._find(path, parent=True)
if name in parent.children:
raise FuseOSError(errno.EEXIST)
now = time.time()
parent.children[name] = Entry(children={}, mode=mode, nlink=2,
size=0, mtime=now, ctime=now)
parent.mtime = now
2014-04-27 22:43:14 +02:00
def open(self, path, flags):
return 0
def read(self, path, size, offset, fh):
self.flush(path, 0)
2014-04-27 22:43:14 +02:00
ent = self._find(path)
assert ent.mode & stat.S_IFREG
2014-05-18 11:34:46 +02:00
data = self.store.get(ent.encpath, format=ent.encformat)
2014-04-27 22:43:14 +02:00
return data[offset:offset + size]
def readdir(self, path, fh):
dirent = self._find(path)
return ['.', '..'] + list(dirent.children)
2014-04-27 22:43:14 +02:00
def readlink(self, path):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def removexattr(self, path, name):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def rename(self, old, new):
self.flush(old, 0)
self._clear_write_cache()
if new.startswith(old):
raise FuseOSError(errno.EINVAL)
with self.transaction():
old_dir, old_name = self._find(old, parent=True)
if old_name not in old_dir.children:
raise FuseOSError(errno.ENOENT)
new_dir, new_name = self._find(new, parent=True)
old_ent = old_dir.children[old_name]
new_ent = new_dir.children.get(new_name)
if new_ent:
if new_ent.mode & stat.S_IFDIR:
if not old_ent.mode & stat.S_IFDIR:
raise FuseOSError(errno.EISDIR)
if new_ent.children:
raise FuseOSError(errno.ENOTEMPTY)
elif old_ent.mode & stat.S_IFDIR:
raise FuseOSError(errno.ENOTDIR)
new_dir.children[new_name] = old_dir.children.pop(old_name)
old_dir.mtime = new_dir.mtime = time.time()
if new_ent != None and new_ent.mode & stat.S_IFREG:
self.store.delete(new_ent.encpath)
2014-04-27 22:43:14 +02:00
def rmdir(self, path):
with self.transaction():
parent, name = self._find(path, parent=True)
if name not in parent.children:
raise FuseOSError(errno.ENOENT)
ent = parent.children[name]
if not ent.mode & stat.S_IFDIR:
raise FuseOSError(errno.ENOTDIR)
if ent.children:
raise FuseOSError(errno.ENOTEMPTY)
del parent.children[name]
parent.mtime = time.time()
2014-04-27 22:43:14 +02:00
def setxattr(self, path, name, value, options, position = 0):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def statfs(self, path):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def symlink(self, target, source):
2014-05-01 19:32:30 +02:00
raise FuseOSError(errno.ENOSYS)
2014-04-27 22:43:14 +02:00
def truncate(self, path, length, fh = None):
self.flush(path, 0)
self._clear_write_cache()
with self.transaction() as putx:
ent = self._find(path)
if length == 0:
2015-12-08 00:15:47 +01:00
buf = b''
else:
2014-05-18 11:34:46 +02:00
buf = self.store.get(ent.encpath, format=ent.encformat)
buf = buf[:length]
ent.encpath = putx(buf, ent.encpath)
ent.size = length
2014-04-27 22:43:14 +02:00
def unlink(self, path):
with self.transaction():
if self.write_path == path:
# no need to flush afterwards
self._clear_write_cache()
parent, name = self._find(path, parent=True)
if name not in parent.children:
raise FuseOSError(errno.ENOENT)
ent = parent.children.pop(name)
parent.mtime = time.time()
self.store.delete(ent.encpath)
2014-04-27 22:43:14 +02:00
def utimens(self, path, times = None):
if times is None:
mtime = time.time()
else:
mtime = times[1]
with self.transaction():
ent = self._find(path)
ent.mtime = mtime
2014-04-27 22:43:14 +02:00
def write(self, path, data, offset, fh):
if path != self.write_path:
self.flush(self.write_path, None)
ent = self._find(path)
2014-05-18 11:34:46 +02:00
buf = self.store.get(ent.encpath, format=ent.encformat)
2014-04-27 22:43:14 +02:00
self.write_buf = [buf]
self.write_len = len(buf)
2014-04-27 22:43:14 +02:00
self.write_path = path
if offset == self.write_len:
2014-04-27 22:43:14 +02:00
self.write_buf.append(data)
self.write_len += len(data)
2014-04-27 22:43:14 +02:00
else:
2015-12-08 00:15:47 +01:00
buf = b''.join(self.write_buf)
2014-04-27 22:43:14 +02:00
buf = buf[:offset] + data + buf[offset + len(data):]
self.write_buf = [buf]
self.write_len = len(buf)
2014-04-27 22:43:14 +02:00
self.write_dirty = True
return len(data)
if __name__ == '__main__':
if len(sys.argv) != 4:
sys.stderr.write('Usage: gpgfs <gpg_keyid> <encrypted_root> <mountpoint>\n')
sys.exit(1)
2014-04-29 21:14:42 +02:00
logpath = os.path.join(os.path.dirname(__file__), 'gpgfs.log')
log.addHandler(logging.FileHandler(logpath, 'w'))
log.setLevel(logging.DEBUG)
2014-04-27 22:43:14 +02:00
fs = GpgFs(sys.argv[2], sys.argv[1])
2014-05-01 19:32:30 +02:00
FUSE(fs, sys.argv[3], foreground=True)