Remove Tree type and adjust API to continue working

This commit is contained in:
Andrew Mulbrook 2026-03-28 23:13:16 -05:00
parent 6470aee802
commit b251614667
4 changed files with 38 additions and 126 deletions

View file

@ -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")

View file

@ -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

View file

@ -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"""
...

View file

@ -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: