From b2516146673f5d8bb7b21a25c8eb0dc0a84e0704 Mon Sep 17 00:00:00 2001 From: Andrew Mulbrook Date: Sat, 28 Mar 2026 23:13:16 -0500 Subject: [PATCH] Remove Tree type and adjust API to continue working --- trovedb/fs.py | 34 ++++--------------------- trovedb/fuse/server.py | 56 ++++++++++++++++++++++-------------------- trovedb/trove.py | 30 +--------------------- trovedb/trovedb.py | 44 +++------------------------------ 4 files changed, 38 insertions(+), 126 deletions(-) diff --git a/trovedb/fs.py b/trovedb/fs.py index 8dc29fe..86aed12 100644 --- a/trovedb/fs.py +++ b/trovedb/fs.py @@ -3,7 +3,7 @@ import datetime as dt from pathlib import Path from typing import Optional, Iterable, Iterator, override -from .trove import Note, Trove, TreeNote, BadNoteType, TreeEntry, NoteNotFound, ObjectId +from .trove import Note, Trove, BadNoteType, TreeEntry, NoteNotFound, ObjectId from . import trove as tr class FSNote(Note): @@ -77,7 +77,7 @@ class FSNote(Note): if mime == 'inode/directory': if content: raise NotImplementedError("FSNote does not support children") - return FSTreeNote(self._trove, self._new_child_subdir(name, False)) + return FSNote(self._trove, self._new_child_subdir(name, False)) ex_path = self._fs_path / name ex_path.write_bytes(content) @@ -143,29 +143,7 @@ class FSNote(Note): for item in self._fs_path.iterdir(): if item.name == ".trove": continue - yield TreeEntry(name=item.name, object_id=item.stat().st_ino) - -class FSTreeNote(FSNote, TreeNote): - def mkdir(self, name: str) -> 'FSTreeNote': - target_path = self._fs_path / name - target_path.mkdir(exist_ok=True) - return FSTreeNote(self._trove, path=target_path) - - def entries(self) -> Iterable[TreeEntry]: - try: - for item in self._fs_path.iterdir(): - if item.name == ".trove": - continue - yield TreeEntry(name=item.name, object_id=str(item)) - except OSError: - pass - - def unlink(self, name: str): - return self.unlink_(name) - - def link(self, name: str, note: Note): - return self.link_(name, note) - + yield TreeEntry(name=item.name, object_id=str(self._fs_path / item.name)) class FSTrove(Trove): @@ -189,8 +167,6 @@ class FSTrove(Trove): def get_raw_note_by_path(self, path: Path) -> Note: if not path.exists(): raise tr.ErrorNotFound(str(path)) - if path.is_dir(): - return FSTreeNote(self, path=path) return FSNote(self, path=path) def get_raw_note(self, note_id: ObjectId) -> Note: @@ -221,8 +197,8 @@ class FSTrove(Trove): def create_blob(self, data: bytes | None = None) -> Note: raise NotImplementedError("FSTrove does not support blobs") - def get_root(self) -> TreeNote: - return FSTreeNote(self, path=self.root) + def get_root(self) -> Note: + return FSNote(self, path=self.root) def _get_metadata(self, inode: int, key: str) -> Optional[bytes]: raise NotImplementedError("FSTrove does not support metadata") diff --git a/trovedb/fuse/server.py b/trovedb/fuse/server.py index 67fef3c..3771d68 100644 --- a/trovedb/fuse/server.py +++ b/trovedb/fuse/server.py @@ -20,7 +20,7 @@ import pyfuse3 import trio from pyfuse3 import InodeT, FileHandleT -from trovedb.trove import Trove, Note, Tree as TroveTree, TreeNote, ObjectId, TreeExists +from trovedb.trove import Trove, Note, ObjectId, TreeExists import trovedb.trove as tr @@ -50,8 +50,12 @@ class _TroveHandle: class _TroveHandleTree(_TroveHandle): @property - def tree(self) -> TreeNote: - return cast(TreeNote, self.note) + def tree(self) -> Note: + return self.note + +def _note_has_folder(note: Note) -> bool: + """Return TRUE if a note name should have an associated folder""" + return note.has_children() or note.mime == "inode/directory" class TroveFuseOps(pyfuse3.Operations): @@ -138,13 +142,11 @@ class TroveFuseOps(pyfuse3.Operations): def _lookup_child(self, parent_inode: InodeT, name: bytes) -> Tuple[_TroveEntry, Note]: parent = self._get_inode_note(parent_inode) - if not isinstance(parent, TreeNote): - raise pyfuse3.FUSEError(errno.ENOTDIR) try: note = parent.child(name.decode()) - except (KeyError, tr.ErrorNotFound): + except tr.ErrorWithErrno as e: logger.debug("lookup failed: %d -> %s", parent_inode, name.decode()) - raise pyfuse3.FUSEError(errno.ENOENT) from None + raise pyfuse3.FUSEError(e.errno) from None ent = self._create_get_ent_from_note(note) return ent, note @@ -160,15 +162,18 @@ class TroveFuseOps(pyfuse3.Operations): # ------------------------------------------------------------------ def _open_handle(self, inode: InodeT) -> _TroveHandle: + logger.debug("open_handle inode:%d", inode) note = self._get_inode_note(inode) handle_id = FileHandleT(self._next_handle) self._next_handle += 1 handle: _TroveHandle - if isinstance(note, TreeNote): + if _note_has_folder(note): + logger.debug("open_handle inode:%d is a folder", inode) handle = _TroveHandleTree(inode_id=inode, handle_id=handle_id, note=note) else: + logger.debug("open_handle inode:%d is a file", inode) handle = _TroveHandle(inode_id=inode, handle_id=handle_id, note=note) self._handles[handle_id] = handle @@ -186,7 +191,9 @@ class TroveFuseOps(pyfuse3.Operations): # Determine basic information is_tree = True size = 0 - if not hasattr(note, 'mkdir'): + + # FIXME: Properly support folder / content, right now it's either or + if not _note_has_folder(note): size = len(note.read_content()) is_tree = False @@ -265,6 +272,7 @@ class TroveFuseOps(pyfuse3.Operations): # ------------------------------------------------------------------ async def opendir(self, inode: InodeT, ctx) -> FileHandleT: + logger.debug("opendir inode:%d", inode) handle = self._open_handle(inode) if not isinstance(handle, _TroveHandleTree): logger.debug("attempted opendir on %d not a tree", inode) @@ -277,10 +285,7 @@ class TroveFuseOps(pyfuse3.Operations): logger.debug("readdir %d start_id %d", fh, start_id) handle = self._get_handle(fh) note = handle.note - if not isinstance(note, TroveTree): - logger.debug("attempted readdir on %d not a tree", fh) - raise pyfuse3.FUSEError(errno.ENOTDIR) - entries = list(note.entries()) # [(name, object_id), ...] + entries = list(note.children()) # [(name, object_id), ...] for idx, entry in enumerate(entries): if idx < start_id: @@ -320,13 +325,10 @@ class TroveFuseOps(pyfuse3.Operations): async def rmdir(self, parent_inode: InodeT, name: bytes, ctx) -> None: logger.debug("rmdir inode:%d name:%s", parent_inode, name) parent = self._get_inode_note(parent_inode) - if not isinstance(parent, TreeNote): - raise pyfuse3.FUSEError(errno.ENOTDIR) try: - parent.unlink(name.decode()) - except KeyError: - raise pyfuse3.FUSEError(errno.ENOENT) from None - + parent.rm_child(name.decode(), False) + except tr.ErrorWithErrno as e: + raise pyfuse3.FUSEError(e.errno) from None # ------------------------------------------------------------------ # File ops @@ -334,9 +336,10 @@ class TroveFuseOps(pyfuse3.Operations): async def open(self, inode: InodeT, flags, ctx) -> pyfuse3.FileInfo: handle = self._open_handle(inode) - if isinstance(handle.note, TroveTree): - self._close_handle(handle) - raise pyfuse3.FUSEError(errno.EISDIR) + # FIXME: Add support for inode tree and inode content + # if isinstance(handle.note, TroveTree): + # self._close_handle(handle) + # raise pyfuse3.FUSEError(errno.EISDIR) return pyfuse3.FileInfo(fh=handle.handle_id) async def create(self, parent_inode: InodeT, name: bytes, mode: int, flags, ctx) -> tuple: @@ -392,14 +395,13 @@ class TroveFuseOps(pyfuse3.Operations): # Grab the parents new_parent = self._get_inode_note(parent_inode_new) - if not isinstance(new_parent, TroveTree): - raise pyfuse3.FUSEError(errno.ENOTDIR) old_parent = self._get_inode_note(parent_inode_old) - if not isinstance(old_parent, TroveTree): - raise pyfuse3.FUSEError(errno.ENOTDIR) # Move! - self._trove.move(old_parent, name_old_str, new_parent, name_new_str, overwrite=True) + try: + self._trove.move(old_parent, name_old_str, new_parent, name_new_str, overwrite=True) + except tr.ErrorWithErrno as e: + raise pyfuse3.FUSEError(e.errno) from None # ------------------------------------------------------------------ # Serve diff --git a/trovedb/trove.py b/trovedb/trove.py index 1bf6182..ef8725c 100644 --- a/trovedb/trove.py +++ b/trovedb/trove.py @@ -124,34 +124,6 @@ def new_child(note: Note, name: str, mime: str = DEFAULT_MIME, content: bytes | return note.new_child(name=name, mime=mime, content=content, executable=executable, hidden=hidden) -@runtime_checkable -class Tree(Protocol): - def link(self, name: str, note: Note): - """Link name to a given note.""" - ... - - def unlink(self, name: str): - """Remove name from the tree.""" - ... - - def mkdir(self, name: str) -> Self: - """Create a new Tree with the given name.""" - ... - - def rmdir(self, name: str) -> None: - """Remove a directory from the tree.""" - ... - - def entries(self) -> Iterable[TreeEntry]: - """Return all entries in the directory""" - ... - - -@runtime_checkable -class TreeNote(Note, Tree, Protocol): - """Tree Note""" - - @runtime_checkable class Trove(Protocol): """ @@ -171,6 +143,6 @@ class Trove(Protocol): """Create a new blob node at the given path with content""" ... - def get_root(self) -> TreeNote: + def get_root(self) -> Note: """Get Tree Node at the given path""" ... diff --git a/trovedb/trovedb.py b/trovedb/trovedb.py index b45807a..f6f3253 100644 --- a/trovedb/trovedb.py +++ b/trovedb/trovedb.py @@ -14,7 +14,7 @@ from .db import Sqlite3Trove, NOTE_ROOT_ID from . import trove as tr -from .trove import Note, Trove, TreeNote, TreeEntry, NoteNotFound, ObjectId +from .trove import Note, Trove, TreeEntry, NoteNotFound, ObjectId class NoteImpl(Note): @@ -79,9 +79,6 @@ class NoteImpl(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) self._db.link(self._object_id, name, object_id) - # 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: @@ -103,39 +100,6 @@ class NoteImpl(Note): 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 # --------------------------------------------------------------------------- @@ -176,8 +140,6 @@ class TroveImpl(Trove): 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) @override @@ -197,9 +159,9 @@ class TroveImpl(Trove): obj_id = self._db.write_blob(data or b"", dtype=dtype) return NoteImpl(self, obj_id) - def get_root(self) -> TreeNote: + def get_root(self) -> Note: """Return the root TreeNote (always id=NOTE_ROOT_ID).""" - return TreeNoteImpl(self, NOTE_ROOT_ID) + return NoteImpl(self, NOTE_ROOT_ID) def open_db_trove(path: str | Path, create: bool = False, **kwargs: tr.OpenArguments) -> Trove: