objectstore: implement a new metadata class
Implement a new class, nested inside `Object`, to read and write metadata. It is indexed by a key and individual pieces of meta- data are stored in separate files. Empty files are not created.
This commit is contained in:
parent
d48f4eb4ff
commit
fec9dcea97
2 changed files with 127 additions and 0 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import contextlib
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
|
@ -22,6 +23,77 @@ class Object:
|
|||
READ = 0
|
||||
WRITE = 1
|
||||
|
||||
class Metadata:
|
||||
"""store and retrieve metadata for an object"""
|
||||
|
||||
def __init__(self, base, folder: Optional[str] = None) -> None:
|
||||
self.base = base
|
||||
self.folder = folder
|
||||
os.makedirs(self.path, exist_ok=True)
|
||||
|
||||
def _path_for_key(self, key) -> str:
|
||||
assert key
|
||||
name = f"{key}.json"
|
||||
return os.path.join(self.path, name)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if not self.folder:
|
||||
return self.base
|
||||
return os.path.join(self.base, self.folder)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def write(self, key):
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf8",
|
||||
dir=self.path,
|
||||
prefix=".",
|
||||
suffix=".tmp.json",
|
||||
delete=True,
|
||||
)
|
||||
|
||||
with tmp as f:
|
||||
yield f
|
||||
|
||||
f.flush()
|
||||
|
||||
# if nothing was written to the file
|
||||
si = os.stat(tmp.name)
|
||||
if si.st_size == 0:
|
||||
return
|
||||
|
||||
dest = self._path_for_key(key)
|
||||
# ensure it is proper json?
|
||||
os.link(tmp.name, dest)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def read(self, key):
|
||||
dest = self._path_for_key(key)
|
||||
try:
|
||||
with open(dest, "r", encoding="utf8") as f:
|
||||
yield f
|
||||
except FileNotFoundError:
|
||||
raise KeyError(f"No metadata for '{key}'") from None
|
||||
|
||||
def set(self, key: str, data):
|
||||
|
||||
if data is None:
|
||||
return
|
||||
|
||||
with self.write(key) as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def get(self, key: str):
|
||||
with contextlib.suppress(KeyError):
|
||||
with self.read(key) as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
def __fspath__(self):
|
||||
return self.path
|
||||
|
||||
def __init__(self, store: "ObjectStore", uid: str, mode: Mode):
|
||||
self._mode = mode
|
||||
self._workdir = None
|
||||
|
|
|
|||
|
|
@ -199,6 +199,61 @@ class TestObjectStore(unittest.TestCase):
|
|||
assert store_path(store, "b", "A")
|
||||
assert store_path(store, "b", "B")
|
||||
|
||||
def test_metadata(self):
|
||||
|
||||
# test metadata object directly first
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
md = objectstore.Object.Metadata(tmp)
|
||||
|
||||
assert os.fspath(md) == tmp
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
with md.read("a"):
|
||||
pass
|
||||
|
||||
# do not write anything to the file, it should not get stored
|
||||
with md.write("a"):
|
||||
pass
|
||||
|
||||
assert len(list(os.scandir(tmp))) == 0
|
||||
|
||||
# also we should not write anything if an exception was raised
|
||||
with self.assertRaises(AssertionError):
|
||||
with md.write("a") as f:
|
||||
f.write("{}")
|
||||
raise AssertionError
|
||||
|
||||
with md.write("a") as f:
|
||||
f.write("{}")
|
||||
|
||||
assert len(list(os.scandir(tmp))) == 1
|
||||
|
||||
with md.read("a") as f:
|
||||
assert f.read() == "{}"
|
||||
|
||||
data = {
|
||||
"boolean": True,
|
||||
"number": 42,
|
||||
"string": "yes, please"
|
||||
}
|
||||
|
||||
extra = {
|
||||
"extra": "data"
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
md = objectstore.Object.Metadata(tmp)
|
||||
|
||||
d = md.get("a")
|
||||
assert d is None
|
||||
|
||||
md.set("a", None)
|
||||
with self.assertRaises(KeyError):
|
||||
with md.read("a"):
|
||||
pass
|
||||
|
||||
md.set("a", data)
|
||||
assert md.get("a") == data
|
||||
def test_host_tree(self):
|
||||
with objectstore.ObjectStore(self.store) as store:
|
||||
host = store.host_tree
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue