Fix write consistency; separate encrypted directory access to GpgStore class
This commit is contained in:
parent
529e86621b
commit
727a8bb123
117
gpgfs.py
117
gpgfs.py
|
@ -1,56 +1,19 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from fuse import FUSE, FuseOSError, Operations
|
from fuse import FUSE, FuseOSError, Operations
|
||||||
import gnupg # python-gnupg
|
|
||||||
import zlib
|
|
||||||
import errno
|
import errno
|
||||||
import stat
|
import stat
|
||||||
from binascii import hexlify
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
|
import gpgstore
|
||||||
|
|
||||||
magic = 'GPGFS1\n'
|
magic = 'GPGFS1\n'
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger('gpgfs')
|
||||||
|
|
||||||
def decrypt(gpg, path):
|
|
||||||
try:
|
|
||||||
data = file(path).read()
|
|
||||||
except IOError, err:
|
|
||||||
log.error("read failed: %s: %s", path, str(err))
|
|
||||||
raise FuseOSError(err.errno)
|
|
||||||
if not data:
|
|
||||||
return data
|
|
||||||
res = gpg.decrypt(data)
|
|
||||||
if not res.ok:
|
|
||||||
log.error("decryption failed, %s: %s", res.status, path)
|
|
||||||
raise FuseOSError(errno.EIO)
|
|
||||||
data = zlib.decompress(res.data)
|
|
||||||
log.debug('decrypted %s' % path)
|
|
||||||
return data
|
|
||||||
|
|
||||||
def encrypt(gpg, keyid, path, data):
|
|
||||||
data = zlib.compress(data, 1)
|
|
||||||
res = gpg.encrypt(data, keyid, armor=False)
|
|
||||||
if not res.ok:
|
|
||||||
log.error("encryption failed (keyid %s), %s: %s",
|
|
||||||
keyid, res.status, path)
|
|
||||||
raise FuseOSError(errno.EIO)
|
|
||||||
try:
|
|
||||||
with file(path+'.tmp', 'w') as fd:
|
|
||||||
fd.write(res.data)
|
|
||||||
os.rename(path+'.tmp', path)
|
|
||||||
except IOError, err:
|
|
||||||
log.error("write failed: %s: %s", path, str(err))
|
|
||||||
raise FuseOSError(err.errno)
|
|
||||||
finally:
|
|
||||||
try: os.remove(path+'.tmp')
|
|
||||||
except: pass
|
|
||||||
log.debug('encrypted %s' % path)
|
|
||||||
|
|
||||||
class Entry:
|
class Entry:
|
||||||
'''
|
'''
|
||||||
|
@ -64,8 +27,8 @@ class Entry:
|
||||||
ENT_FILE = 0
|
ENT_FILE = 0
|
||||||
ENT_DIR = 1
|
ENT_DIR = 1
|
||||||
|
|
||||||
def read_index(gpg, path):
|
def read_index(store, path):
|
||||||
data = decrypt(gpg, path)
|
data = store.get(path)
|
||||||
buf = StringIO(data)
|
buf = StringIO(data)
|
||||||
if buf.read(len(magic)) != magic:
|
if buf.read(len(magic)) != magic:
|
||||||
raise IOError, 'index parse error: %s' % path
|
raise IOError, 'index parse error: %s' % path
|
||||||
|
@ -73,13 +36,13 @@ def read_index(gpg, path):
|
||||||
root = Entry(**read_dict(buf))
|
root = Entry(**read_dict(buf))
|
||||||
return root
|
return root
|
||||||
|
|
||||||
def write_index(gpg, keyid, path, root):
|
def write_index(store, path, root):
|
||||||
buf = StringIO()
|
buf = StringIO()
|
||||||
buf.write(magic)
|
buf.write(magic)
|
||||||
header = ''
|
header = ''
|
||||||
write_atom(buf, header)
|
write_atom(buf, header)
|
||||||
write_dict(buf, root)
|
write_dict(buf, root)
|
||||||
encrypt(gpg, keyid, path, buf.getvalue())
|
store.put(buf.getvalue(), path=path)
|
||||||
|
|
||||||
def write_dict(fd, dct):
|
def write_dict(fd, dct):
|
||||||
# breadth-first
|
# breadth-first
|
||||||
|
@ -167,12 +130,11 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
self.encroot = encroot.rstrip('/')
|
self.encroot = encroot.rstrip('/')
|
||||||
assert os.path.exists(self.encroot)
|
assert os.path.exists(self.encroot)
|
||||||
assert os.path.isdir(self.encroot)
|
assert os.path.isdir(self.encroot)
|
||||||
self.keyid = keyid
|
|
||||||
#self.cache = cache
|
#self.cache = cache
|
||||||
self.gpg = gnupg.GPG()
|
self.store = gpgstore.GpgStore(self.encroot, keyid)
|
||||||
self.index_path = self.encroot + '/index'
|
self.index_path = 'index'
|
||||||
if os.path.exists(self.index_path):
|
if os.path.exists(self.encroot + '/' + self.index_path):
|
||||||
self.root = read_index(self.gpg, self.index_path)
|
self.root = read_index(self.store, self.index_path)
|
||||||
else:
|
else:
|
||||||
self.root = Entry(type=ENT_DIR, children={},
|
self.root = Entry(type=ENT_DIR, children={},
|
||||||
st_mode=0755,
|
st_mode=0755,
|
||||||
|
@ -183,8 +145,8 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
self.fd = 0
|
self.fd = 0
|
||||||
self._clear_write_cache()
|
self._clear_write_cache()
|
||||||
|
|
||||||
def _write_index(self, suffix=''):
|
def _write_index(self):
|
||||||
write_index(self.gpg, self.keyid, self.index_path + suffix, self.root)
|
write_index(self.store, self.index_path, self.root)
|
||||||
|
|
||||||
def _find(self, path, parent=False):
|
def _find(self, path, parent=False):
|
||||||
assert path.startswith('/')
|
assert path.startswith('/')
|
||||||
|
@ -229,17 +191,11 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
raise FuseOSError(errno.ENOSYS)
|
raise FuseOSError(errno.ENOSYS)
|
||||||
|
|
||||||
def create(self, path, mode):
|
def create(self, path, mode):
|
||||||
encpath = hexlify(os.urandom(20))
|
|
||||||
encpath = encpath[:2] + '/' + encpath[2:]
|
|
||||||
dir, path = self._find(path, parent=True)
|
dir, path = self._find(path, parent=True)
|
||||||
if path in dir.children:
|
if path in dir.children:
|
||||||
raise FuseOSError(errno.EEXIST)
|
raise FuseOSError(errno.EEXIST)
|
||||||
encdir = self.encroot + '/' + encpath[:2]
|
# FIXME mode
|
||||||
if not os.path.exists(encdir):
|
encpath = self.store.put('')
|
||||||
os.mkdir(encdir, 0755)
|
|
||||||
fd = os.open(self.encroot + '/' + encpath,
|
|
||||||
os.O_WRONLY | os.O_CREAT, mode & 0777)
|
|
||||||
os.close(fd)
|
|
||||||
prev_mtime = dir.st_mtime
|
prev_mtime = dir.st_mtime
|
||||||
dir.children[path] = Entry(type=ENT_FILE, path=encpath, st_size=0)
|
dir.children[path] = Entry(type=ENT_FILE, path=encpath, st_size=0)
|
||||||
log.debug('new path %s => %s', path, encpath)
|
log.debug('new path %s => %s', path, encpath)
|
||||||
|
@ -247,7 +203,7 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
try:
|
try:
|
||||||
self._write_index()
|
self._write_index()
|
||||||
except:
|
except:
|
||||||
try: os.remove(self.encroot + '/' + encpath)
|
try: self.store.delete(encpath)
|
||||||
except: pass
|
except: pass
|
||||||
del dir.children[path]
|
del dir.children[path]
|
||||||
dir.st_mtime = prev_mtime
|
dir.st_mtime = prev_mtime
|
||||||
|
@ -259,26 +215,29 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
if not self.write_dirty:
|
if not self.write_dirty:
|
||||||
log.debug('nothing to flush')
|
log.debug('nothing to flush')
|
||||||
return 0
|
return 0
|
||||||
ent = self._find(self.write_path)
|
|
||||||
encpath = self.encroot + '/' + ent.path
|
|
||||||
buf = ''.join(self.write_buf)
|
buf = ''.join(self.write_buf)
|
||||||
self.write_buf = [buf]
|
self.write_buf = [buf]
|
||||||
encrypt(self.gpg, self.keyid, encpath+'.new', buf)
|
ent = self._find(self.write_path)
|
||||||
prev_size = ent.st_size
|
prev_size = ent.st_size
|
||||||
|
prev_path = ent.path
|
||||||
ent.st_size = len(buf)
|
ent.st_size = len(buf)
|
||||||
|
ent.path = self.store.put(buf)
|
||||||
try:
|
try:
|
||||||
self._write_index(suffix='.new')
|
self._write_index()
|
||||||
except:
|
except:
|
||||||
os.remove(encpath+'.new')
|
self.store.delete(ent.path)
|
||||||
ent.st_size = prev_size
|
ent.st_size = prev_size
|
||||||
|
ent.path = prev_path
|
||||||
raise
|
raise
|
||||||
# FIXME renames cannot fail, right?
|
self.store.delete(prev_path)
|
||||||
os.rename(encpath+'.new', encpath)
|
|
||||||
os.rename(self.index_path+'.new', self.index_path)
|
|
||||||
self.write_dirty = False
|
self.write_dirty = False
|
||||||
log.debug('flushed %d bytes to %s', len(buf), self.write_path)
|
log.debug('flushed %d bytes to %s', len(buf), self.write_path)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def fsync(self, path, datasync, fh):
|
||||||
|
self.flush(path, fh)
|
||||||
|
return 0
|
||||||
|
|
||||||
def getattr(self, path, fh = None):
|
def getattr(self, path, fh = None):
|
||||||
ent = self._find(path)
|
ent = self._find(path)
|
||||||
if ent.type == ENT_DIR:
|
if ent.type == ENT_DIR:
|
||||||
|
@ -324,8 +283,7 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
self.flush(path, 0)
|
self.flush(path, 0)
|
||||||
ent = self._find(path)
|
ent = self._find(path)
|
||||||
assert ent.type == ENT_FILE
|
assert ent.type == ENT_FILE
|
||||||
encpath = self.encroot + '/' + ent.path
|
data = self.store.get(ent.path)
|
||||||
data = decrypt(self.gpg, encpath)
|
|
||||||
return data[offset:offset + size]
|
return data[offset:offset + size]
|
||||||
|
|
||||||
def readdir(self, path, fh):
|
def readdir(self, path, fh):
|
||||||
|
@ -404,25 +362,23 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
self.flush(path, 0)
|
self.flush(path, 0)
|
||||||
self._clear_write_cache()
|
self._clear_write_cache()
|
||||||
ent = self._find(path)
|
ent = self._find(path)
|
||||||
encpath = self.encroot + '/' + ent.path
|
|
||||||
if length == 0:
|
if length == 0:
|
||||||
with open(encpath+'.new', 'w'):
|
buf = ''
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
buf = decrypt(self.gpg, encpath)
|
buf = self.store.get(ent.path)
|
||||||
buf = buf[:length]
|
buf = buf[:length]
|
||||||
encrypt(self.gpg, self.keyid, encpath+'.new', buf)
|
|
||||||
prev_size = ent.st_size
|
prev_size = ent.st_size
|
||||||
|
prev_path = ent.path
|
||||||
ent.st_size = length
|
ent.st_size = length
|
||||||
|
ent.path = self.store.put(buf)
|
||||||
try:
|
try:
|
||||||
self._write_index(suffix='.new')
|
self._write_index()
|
||||||
except:
|
except:
|
||||||
|
os.remove(ent.path)
|
||||||
ent.st_size = prev_size
|
ent.st_size = prev_size
|
||||||
os.remove(encpath+'.new')
|
ent.path = prev_path
|
||||||
raise
|
raise
|
||||||
# FIXME renames cannot fail, right?
|
self.store.delete(prev_path)
|
||||||
os.rename(encpath+'.new', encpath)
|
|
||||||
os.rename(self.index_path+'.new', self.index_path)
|
|
||||||
|
|
||||||
def unlink(self, path):
|
def unlink(self, path):
|
||||||
if self.write_path == path:
|
if self.write_path == path:
|
||||||
|
@ -465,10 +421,9 @@ class GpgFs(LoggingMixIn, Operations):
|
||||||
|
|
||||||
def write(self, path, data, offset, fh):
|
def write(self, path, data, offset, fh):
|
||||||
ent = self._find(path)
|
ent = self._find(path)
|
||||||
encpath = self.encroot + '/' + ent.path
|
|
||||||
if path != self.write_path:
|
if path != self.write_path:
|
||||||
self.flush(self.write_path, None)
|
self.flush(self.write_path, None)
|
||||||
buf = decrypt(self.gpg, encpath)
|
buf = self.store.get(ent.path)
|
||||||
self.write_buf = [buf]
|
self.write_buf = [buf]
|
||||||
self.write_len = len(buf)
|
self.write_len = len(buf)
|
||||||
self.write_path = path
|
self.write_path = path
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
|
||||||
|
import os
|
||||||
|
import gnupg
|
||||||
|
from binascii import hexlify
|
||||||
|
import zlib
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger('gpgfs')
|
||||||
|
|
||||||
|
class GpgStore(object):
|
||||||
|
def __init__(self, encroot, keyid):
|
||||||
|
self.encroot = encroot
|
||||||
|
self.keyid = keyid
|
||||||
|
self.gpg = gnupg.GPG()
|
||||||
|
|
||||||
|
def put(self, data, path=None):
|
||||||
|
if not path:
|
||||||
|
path = hexlify(os.urandom(20))
|
||||||
|
path = path[:2] + '/' + path[2:]
|
||||||
|
encdir = self.encroot + '/' + path[:2]
|
||||||
|
if not os.path.exists(encdir):
|
||||||
|
os.mkdir(encdir, 0755)
|
||||||
|
data = zlib.compress(data, 1)
|
||||||
|
res = self.gpg.encrypt(data, self.keyid, armor=False)
|
||||||
|
if not res.ok:
|
||||||
|
log.error("encryption failed (keyid %s), %s: %s",
|
||||||
|
self.keyid, res.status, path)
|
||||||
|
raise OSError(errno.EIO)
|
||||||
|
try:
|
||||||
|
with file(self.encroot + '/' + path + '.tmp', 'w') as fd:
|
||||||
|
fd.write(res.data)
|
||||||
|
os.rename(self.encroot + '/' + path + '.tmp',
|
||||||
|
self.encroot + '/' + path)
|
||||||
|
except IOError, err:
|
||||||
|
log.error("write failed: %s: %s", path, str(err))
|
||||||
|
raise OSError(err.errno)
|
||||||
|
finally:
|
||||||
|
try: os.remove(self.encroot + '/' + path + '.tmp')
|
||||||
|
except: pass
|
||||||
|
log.debug('encrypted %s' % path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
try:
|
||||||
|
data = file(self.encroot + '/' + path).read()
|
||||||
|
except OSError, err:
|
||||||
|
log.error("read failed: %s: %s", path, str(err))
|
||||||
|
raise
|
||||||
|
if not data:
|
||||||
|
return data
|
||||||
|
res = self.gpg.decrypt(data)
|
||||||
|
if not res.ok:
|
||||||
|
log.error("decryption failed, %s: %s", res.status, path)
|
||||||
|
raise OSError(errno.EIO)
|
||||||
|
data = zlib.decompress(res.data)
|
||||||
|
log.debug('decrypted %s' % path)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def delete(self, path):
|
||||||
|
os.remove(self.encroot + '/' + path)
|
||||||
|
log.debug('deleted %s' % path)
|
Loading…
Reference in New Issue