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:
parent
c73a28613b
commit
be8aafbb90
1 changed files with 44 additions and 5 deletions
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue