We want trees to just be notes. We want to reference notes by Path instead of object ids. Stop thinking about the fs implementation and the fuse service with the same terms - they will be different backends.
194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
"""
|
|
trovedb.py — Concrete implementation of Trove protocols backed by Sqlite3Trove.
|
|
|
|
Implements BlobNote, TreeNote, and Trove protocols defined in trove.py.
|
|
Depends on db.py (Sqlite3Trove) for storage.
|
|
"""
|
|
|
|
from typing import Optional, Iterator
|
|
from pathlib import Path
|
|
import datetime as dt
|
|
import uuid
|
|
|
|
from .db import Sqlite3Trove, NOTE_ROOT_ID
|
|
|
|
from . import trove as tr
|
|
|
|
from .trove import Note, Trove, TreeNote, TreeEntry, NoteNotFound, ObjectId
|
|
|
|
|
|
class NoteImpl(Note):
|
|
"""Concrete note implementation."""
|
|
|
|
def __init__(self, parent: 'TroveImpl', object_id: ObjectId):
|
|
if not isinstance(object_id, uuid.UUID):
|
|
object_id = uuid.UUID(str(object_id))
|
|
assert isinstance(object_id, uuid.UUID)
|
|
|
|
self._parent = parent
|
|
self._db = parent.db
|
|
self._object_id: uuid.UUID = object_id
|
|
|
|
@staticmethod
|
|
def get_impl_id(note: Note) -> uuid.UUID:
|
|
if not isinstance(note.object_id, uuid.UUID):
|
|
raise TypeError("Note not compatible with NoteImpl")
|
|
return note.object_id
|
|
|
|
# Note protocol
|
|
@property
|
|
def object_id(self) -> ObjectId:
|
|
return self._object_id
|
|
|
|
@property
|
|
def readonly(self) -> bool:
|
|
return False
|
|
|
|
@property
|
|
def mtime(self) -> dt.datetime:
|
|
"""Return modification time as UTC datetime."""
|
|
mtime = self._db.get_mtime(self._object_id)
|
|
return mtime if mtime is not None else dt.datetime.now(tz=dt.timezone.utc)
|
|
|
|
@property
|
|
def mime(self) -> str:
|
|
"""Return MIME type from the objects table."""
|
|
info = self._db.get_info(self._object_id)
|
|
return info.type if info else "application/octet-stream"
|
|
|
|
def get_raw_metadata(self, key: str) -> Optional[bytes]:
|
|
return self._db.read_metadata(self._object_id, key)
|
|
|
|
def set_raw_metadata(self, key: str, value: bytes) -> None:
|
|
self._db.write_metadata(self._object_id, key, value)
|
|
|
|
def read_content(self) -> bytes:
|
|
data = self._db.read_object(self._object_id)
|
|
return data if data is not None else b""
|
|
|
|
def write_content(self, data: bytes) -> None:
|
|
self._db.write_content(self._object_id, data)
|
|
|
|
def children(self) -> Iterator[TreeEntry]:
|
|
"""Get all children of this note."""
|
|
for name, object_id in self._db.list_tree(self._object_id).items():
|
|
yield TreeEntry(name, object_id)
|
|
|
|
def new_child(self, name: str, mime: str, content: bytes | None, executable: bool, hidden: bool) -> Note:
|
|
"""Create a new child note."""
|
|
content = content if content is not None else b""
|
|
object_id = self._db.write_blob(data=content, object_id=None, dtype=mime, executable=executable, hidden=hidden)
|
|
# TODO fix this
|
|
if mime == 'inode/directory':
|
|
return TreeNoteImpl(self._parent, object_id)
|
|
return NoteImpl(self._parent, object_id)
|
|
|
|
def child(self, name: str) -> Note:
|
|
"""Retrieve a child note by name."""
|
|
entries = self._db.list_tree(self._object_id)
|
|
if name not in entries:
|
|
raise tr.ErrorNotFound(name)
|
|
child_id = entries[name]
|
|
value = self._parent.get_raw_note(child_id)
|
|
if value is None:
|
|
raise tr.ErrorNotFound("dangling child link") # FIXME: better errors
|
|
return value
|
|
|
|
def rm_child(self, name: str, recurse: bool) -> None:
|
|
"""Remove a child note."""
|
|
note = self.child(name)
|
|
if note.has_children() and not recurse:
|
|
raise tr.ErrorNotEmpty(name)
|
|
self._db.unlink(self._object_id, name)
|
|
|
|
|
|
|
|
class TreeNoteImpl(NoteImpl, TreeNote):
|
|
"""Concrete TreeNote: a tree object backed by the tree_entries table."""
|
|
|
|
# Tree protocol
|
|
def link(self, name: str, note: Note) -> None:
|
|
"""Link name to an existing note."""
|
|
self._db.link(self._object_id, name, NoteImpl.get_impl_id(note))
|
|
|
|
def unlink(self, name: str) -> None:
|
|
"""Remove an entry by name."""
|
|
self._db.unlink(self._object_id, name)
|
|
|
|
def mkdir(self, name: str) -> 'TreeNoteImpl':
|
|
"""Create a new empty tree, link it under name, and return it."""
|
|
new_id = self._db.write_tree(b"")
|
|
tree = TreeNoteImpl(self._parent, new_id)
|
|
self.link(name, tree)
|
|
return tree
|
|
|
|
def rmdir(self, name: str) -> None:
|
|
"""Remove a directory from the tree."""
|
|
self.unlink(name)
|
|
|
|
|
|
|
|
def entries(self):
|
|
"""Return all entries as an iterable of TreeEntry."""
|
|
for name, object_id in self._db.list_tree(self._object_id).items():
|
|
yield TreeEntry(name, object_id)
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Trove
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TroveImpl:
|
|
"""
|
|
Concrete Trove: top-level API backed by a Sqlite3Trove database.
|
|
|
|
Use TroveImpl.open() to get an instance.
|
|
"""
|
|
|
|
def __init__(self, db: Sqlite3Trove):
|
|
self._db = db
|
|
|
|
@classmethod
|
|
def open(cls, path: str | Path, create: bool = False) -> "TroveImpl":
|
|
db = Sqlite3Trove.open(path, create=create)
|
|
return cls(db)
|
|
|
|
@property
|
|
def db(self) -> Sqlite3Trove:
|
|
return self._db
|
|
|
|
def close(self) -> None:
|
|
self._db.close()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_):
|
|
self.close()
|
|
|
|
# Trove protocol
|
|
def get_raw_note(self, note_id: ObjectId) -> Note:
|
|
"""Return a BlobNote or TreeNote for the given id, or None if not found."""
|
|
if not isinstance(note_id, uuid.UUID):
|
|
note_id = uuid.UUID(str(note_id))
|
|
info = self._db.get_info(note_id)
|
|
if info is None:
|
|
raise NoteNotFound(note_id)
|
|
if self._db.is_tree(note_id) or info.type == "inode/directory":
|
|
return TreeNoteImpl(self, note_id)
|
|
return NoteImpl(self, note_id)
|
|
|
|
def create_blob(self, data: bytes | None = None,
|
|
dtype: str = "application/octet-stream") -> Note:
|
|
"""Create a new blob object and return a BlobNote for it."""
|
|
obj_id = self._db.write_blob(data or b"", dtype=dtype)
|
|
return NoteImpl(self, obj_id)
|
|
|
|
def get_root(self) -> TreeNote:
|
|
"""Return the root TreeNote (always id=NOTE_ROOT_ID)."""
|
|
return TreeNoteImpl(self, NOTE_ROOT_ID)
|
|
|
|
|
|
def open_db_trove(path: str | Path, create: bool = False, **kwargs: tr.OpenArguments) -> Trove:
|
|
return TroveImpl.open(path, create=create)
|