""" DBM based backend implementation - namespaces are directories, keys and vals go into DBM database. """ import dbm import os from pathlib import Path import random import re import shutil import stat from borgstore.backends.posixfs import PosixFS from ._base import BackendBase, ItemInfo, validate_name from .errors import BackendMustBeOpen, ObjectNotFound from ..constants import TMP_SUFFIX DbmError = dbm.error[0] def get_dbm_backend(url): # dbm:///var/backups/borgstore/first file_regex = r""" dbm:// (?P(/.*)) """ m = re.match(file_regex, url, re.VERBOSE) if m: return Dbm(path=m["path"]) class Dbm(PosixFS): def __init__(self, path): super().__init__(path) def rmdir(self, name): if not self.opened: raise BackendMustBeOpen() path = self._validate_join(name) if not path.exists(): raise ObjectNotFound(name) from None else: if (path / "keyval.dbm").exists(): os.unlink((path / "keyval.dbm")) path.rmdir() def info(self, name): if not self.opened: raise BackendMustBeOpen() path = self._validate_join(name) try: st = path.stat() except FileNotFoundError: db_dir = path.parent try: st = db_dir.stat() except FileNotFoundError: return ItemInfo(name=path.name, exists=False, directory=False, size=0) else: assert stat.S_ISDIR(st.st_mode) key = path.name try: # Do not create if does not exist- we may be querying a directory # composing the namespace. with dbm.open(str(db_dir / "keyval.dbm"), "r") as db: val = db[key] return ItemInfo(name=path.name, exists=True, directory=False, size=len(val)) except (KeyError, DbmError): return ItemInfo(name=path.name, exists=False, directory=False, size=0) else: assert stat.S_ISDIR(st.st_mode) return ItemInfo(name=path.name, exists=True, directory=True, size=0) def load(self, name, *, size=None, offset=0): if not self.opened: raise BackendMustBeOpen() path = self._validate_join(name) db_dir = path.parent try: key = path.name with dbm.open(str(db_dir / "keyval.dbm"), "r") as db: val = db[key] return val[offset:offset+size] if size else val[offset:] except (KeyError, DbmError): raise ObjectNotFound(name) from None def store(self, name, value): if not self.opened: raise BackendMustBeOpen() path = self._validate_join(name) db_dir = path.parent db_dir.mkdir(parents=True, exist_ok=True) key = path.name with dbm.open(str(db_dir / "keyval.dbm"), "c") as db: db[key] = value def delete(self, name): if not self.opened: raise BackendMustBeOpen() path = self._validate_join(name) db_dir = path.parent try: with dbm.open(str(db_dir / "keyval.dbm"), "w") as db: del db[path.name] except (KeyError, DbmError): raise ObjectNotFound(name) from None def move(self, curr_name, new_name): if not self.opened: raise BackendMustBeOpen() curr_path = self._validate_join(curr_name) new_path = self._validate_join(new_name) curr_db_dir = curr_path.parent new_db_dir = new_path.parent if curr_db_dir == new_db_dir: with dbm.open(str(curr_db_dir / "keyval.dbm"), "c") as db: val = db[curr_path.name] del db[curr_path.name] db[new_path.name] = val else: try: new_db_dir.mkdir(parents=True, exist_ok=True) with dbm.open(str(curr_db_dir / "keyval.dbm"), "c") as cdb, dbm.open(str(new_db_dir / "keyval.dbm"), "c") as ndb: val = cdb[curr_path.name] del cdb[curr_path.name] ndb[new_path.name] = val except DbmError: raise ObjectNotFound(curr_name) from None def list(self, name): if not self.opened: raise BackendMustBeOpen() maybe_db_dir = self._validate_join(name) try: # Do not create if does not exist- we may be querying a directory # composing the namespace. with dbm.open(str(maybe_db_dir / "keyval.dbm"), "r") as db: for k in db.keys(): # Required by borgstore to not list these. if not k.decode().endswith(TMP_SUFFIX): yield ItemInfo(name=k.decode(), exists=True, size=len(db[k]), directory=False) except DbmError: pass # Directories are not contained within the DBMs, but we must list them. for f in super().list(name): if f.name in ("keyval.dbm") and not f.directory: pass else: yield f