objectstore: Object.read() for read only access

Provide a way to read the current contents of the object, in a way
the follows the copy-on-write semantics: If `base` is set but the
object has not yet been written to, the `base` content will be
exposed. If no base is set or the object has been written to, the
current (temporary) tree will be exposed. In either way it is done
via a bind mount so it is assured that the contents indeed can only
be read from, but not written to.
The code also currently make sure that there is no write operation
started as long as there is at least one reader.
Additionally, also introduce checks that the object is intact, i.e.
not cleaned up, for all operations that require such a state.
This commit is contained in:
Christian Kellner 2020-02-18 16:34:01 +01:00 committed by Tom Gundersen
parent c73a28613b
commit be8aafbb90

View file

@ -54,6 +54,7 @@ def umount(target, lazy=True):
class Object: class Object:
def __init__(self, store: "ObjectStore"): def __init__(self, store: "ObjectStore"):
self._init = True self._init = True
self._readers = 0
self._base = None self._base = None
self._workdir = None self._workdir = None
self._tree = None self._tree = None
@ -62,6 +63,8 @@ class Object:
def init(self) -> None: def init(self) -> None:
"""Initialize the object with content of its base""" """Initialize the object with content of its base"""
self._check_writable()
self._check_readers()
if self._init: if self._init:
return return
@ -99,9 +102,23 @@ class Object:
def write(self) -> str: def write(self) -> str:
"""Return a path that can be written to""" """Return a path that can be written to"""
self._check_writable()
self._check_readers()
self.init() self.init()
return self._tree return self._tree
@contextlib.contextmanager
def read(self) -> str:
self._check_writable()
with self.tempdir("mount") as target:
mount(self._path, target)
try:
self._readers += 1
yield target
finally:
umount(target)
self._readers -= 1
def store_tree(self, destination: str): def store_tree(self, destination: str):
"""Store the tree at destination and reset itself """Store the tree at destination and reset itself
@ -109,6 +126,8 @@ class Object:
target already exist, does nothing. Afterwards it target already exist, does nothing. Afterwards it
resets itself and can be used as if it was new. resets itself and can be used as if it was new.
""" """
self._check_writable()
self._check_readers()
self.init() self.init()
with suppress_oserror(errno.ENOTEMPTY, errno.EEXIST): with suppress_oserror(errno.ENOTEMPTY, errno.EEXIST):
os.rename(self._tree, destination) os.rename(self._tree, destination)
@ -122,20 +141,40 @@ class Object:
self._init = not self._base self._init = not self._base
def cleanup(self): def cleanup(self):
self._check_readers()
if self._workdir: if self._workdir:
self._workdir.cleanup() self._workdir.cleanup()
self._workdir = None self._workdir = None
def _check_readers(self):
"""Internal: Raise a ValueError if there are readers"""
if self._readers:
raise ValueError("Read operation is ongoing")
def _check_writable(self):
"""Internal: Raise a ValueError if not writable"""
if not self._workdir:
raise ValueError("Object is not writable")
@contextlib.contextmanager @contextlib.contextmanager
def _open(self): def _open(self):
"""Open the directory and return the file descriptor""" """Open the directory and return the file descriptor"""
fd = os.open(self._path, os.O_DIRECTORY) with self.read() as path:
try: fd = os.open(path, os.O_DIRECTORY)
yield fd try:
finally: yield fd
os.close(fd) finally:
os.close(fd)
def tempdir(self, suffix=None):
workdir = self._workdir.name
if suffix:
suffix = "-" + suffix
return tempfile.TemporaryDirectory(dir=workdir,
suffix=suffix)
def __enter__(self): def __enter__(self):
self._check_writable()
return self return self
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):