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:
def __init__(self, store: "ObjectStore"):
self._init = True
self._readers = 0
self._base = None
self._workdir = None
self._tree = None
@ -62,6 +63,8 @@ class Object:
def init(self) -> None:
"""Initialize the object with content of its base"""
self._check_writable()
self._check_readers()
if self._init:
return
@ -99,9 +102,23 @@ class Object:
def write(self) -> str:
"""Return a path that can be written to"""
self._check_writable()
self._check_readers()
self.init()
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):
"""Store the tree at destination and reset itself
@ -109,6 +126,8 @@ class Object:
target already exist, does nothing. Afterwards it
resets itself and can be used as if it was new.
"""
self._check_writable()
self._check_readers()
self.init()
with suppress_oserror(errno.ENOTEMPTY, errno.EEXIST):
os.rename(self._tree, destination)
@ -122,20 +141,40 @@ class Object:
self._init = not self._base
def cleanup(self):
self._check_readers()
if self._workdir:
self._workdir.cleanup()
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
def _open(self):
"""Open the directory and return the file descriptor"""
fd = os.open(self._path, os.O_DIRECTORY)
try:
yield fd
finally:
os.close(fd)
with self.read() as path:
fd = os.open(path, os.O_DIRECTORY)
try:
yield 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):
self._check_writable()
return self
def __exit__(self, exc_type, exc_val, exc_tb):