Start API refactor to remove separate Tree

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.
This commit is contained in:
Andrew Mulbrook 2026-03-26 21:00:35 -05:00
parent abbef64bbc
commit 41480a39c9
5 changed files with 245 additions and 234 deletions

View file

@ -20,7 +20,9 @@ import pyfuse3
import trio
from pyfuse3 import InodeT, FileHandleT
from trovedb.trove import Trove, Note, Tree as TroveTree, TreeNote, Blob as TroveBlob, ObjectId, TreeExists
from trovedb.trove import Trove, Note, Tree as TroveTree, TreeNote, ObjectId, TreeExists
import trovedb.trove as tr
logger = logging.getLogger(__name__)
@ -140,7 +142,7 @@ class TroveFuseOps(pyfuse3.Operations):
raise pyfuse3.FUSEError(errno.ENOTDIR)
try:
note = parent.child(name.decode())
except KeyError:
except (KeyError, tr.ErrorNotFound):
logger.debug("lookup failed: %d -> %s", parent_inode, name.decode())
raise pyfuse3.FUSEError(errno.ENOENT) from None
ent = self._create_get_ent_from_note(note)
@ -184,8 +186,8 @@ class TroveFuseOps(pyfuse3.Operations):
# Determine basic information
is_tree = True
size = 0
if isinstance(note, TroveBlob):
size = len(note.read())
if not hasattr(note, 'mkdir'):
size = len(note.read_content())
is_tree = False
# Create and fill attr structure
@ -239,13 +241,13 @@ class TroveFuseOps(pyfuse3.Operations):
ent = self._get_ent_from_inode(inode)
note = self._get_ent_note(ent)
if fields.update_size:
if isinstance(note, TroveBlob):
current = note.read()
if not hasattr(note, 'mkdir'):
current = note.read_content()
new_size = attr.st_size
if new_size < len(current):
note.write(current[:new_size])
note.write_content(current[:new_size])
elif new_size > len(current):
note.write(current + b"\x00" * (new_size - len(current)))
note.write_content(current + b"\x00" * (new_size - len(current)))
else:
raise pyfuse3.FUSEError(errno.EINVAL)
return self._get_attr(ent, note)
@ -278,13 +280,13 @@ class TroveFuseOps(pyfuse3.Operations):
if not isinstance(note, TroveTree):
logger.debug("attempted readdir on %d not a tree", fh)
raise pyfuse3.FUSEError(errno.ENOTDIR)
entries = list(note.list().items()) # [(name, object_id), ...]
entries = list(note.entries()) # [(name, object_id), ...]
for idx, (name, child_id) in enumerate(entries):
for idx, entry in enumerate(entries):
if idx < start_id:
continue
child = self._trove.get_raw_note(child_id)
child = self._trove.get_raw_note(entry.object_id)
if child is None:
continue
@ -292,7 +294,7 @@ class TroveFuseOps(pyfuse3.Operations):
attr = self._get_attr(child_ent, child)
self._ref_entry(child_ent)
if not pyfuse3.readdir_reply(token, name.encode(), attr, idx + 1):
if not pyfuse3.readdir_reply(token, entry.name.encode(), attr, idx + 1):
break
async def releasedir(self, fh: FileHandleT) -> None:
@ -302,19 +304,18 @@ class TroveFuseOps(pyfuse3.Operations):
async def mkdir(self, parent_inode: InodeT, name: bytes, mode: int, ctx) -> pyfuse3.EntryAttributes:
logger.debug("mkdir inode:%d name:%s", parent_inode, name)
# Grab parent note, verify is tree
parent = self._get_inode_note(parent_inode)
if not isinstance(parent, TreeNote):
raise pyfuse3.FUSEError(errno.ENOTDIR)
# Create new directory in note
# TODO: consider implications here, maybe look at ext on dir for mime?
try:
new_tree: TreeNote = parent.mkdir(name.decode())
except TreeExists:
raise pyfuse3.FUSEError(errno.EEXIST) from None
note = tr.new_child(parent, name.decode(), mime='inode/directory')
except tr.ErrorWithErrno as e:
raise pyfuse3.FUSEError(e.errno) from None
# Grab entity for kernel
ent = self._create_get_ent_from_note(new_tree)
ent = self._create_get_ent_from_note(note)
self._ref_entry(ent)
return self._get_attr(ent, new_tree)
return self._get_attr(ent, note)
async def rmdir(self, parent_inode: InodeT, name: bytes, ctx) -> None:
logger.debug("rmdir inode:%d name:%s", parent_inode, name)
@ -341,37 +342,35 @@ class TroveFuseOps(pyfuse3.Operations):
async def create(self, parent_inode: InodeT, name: bytes, mode: int, flags, ctx) -> tuple:
logger.debug("create inode:%d name:%s", parent_inode, name)
parent = self._get_inode_note(parent_inode)
if not isinstance(parent, TroveTree):
raise pyfuse3.FUSEError(errno.ENOTDIR)
name_str = name.decode()
if name_str in parent.list():
raise pyfuse3.FUSEError(errno.EEXIST)
blob = self._trove.create_blob(b"")
parent.link(name_str, blob)
ent = self._create_get_ent_from_note(blob)
# TODO: handle mode
# TODO: handle flags
name_str = name.decode()
note = tr.new_child(parent, name_str)
ent = self._create_get_ent_from_note(note)
self._ref_entry(ent)
handle = self._open_handle(ent.sys_inode)
attr = self._get_attr(ent, blob)
attr = self._get_attr(ent, note)
return pyfuse3.FileInfo(fh=handle.handle_id), attr
async def read(self, fh: FileHandleT, offset: int, length: int) -> bytes:
logger.debug("read fh:%d offset:%d length:%d", fh, offset, length)
handle = self._get_handle(fh)
note = handle.note
if isinstance(note, TroveBlob):
return note.read()[offset:offset + length]
if not hasattr(note, 'mkdir'):
return note.read_content()[offset:offset + length]
raise pyfuse3.FUSEError(errno.EBADF)
async def write(self, fh: FileHandleT, offset: int, data: bytes) -> int:
handle = self._get_handle(fh)
note = handle.note
if isinstance(note, TroveBlob):
existing = note.read()
if not hasattr(note, 'mkdir'):
existing = note.read_content()
if offset > len(existing):
existing = existing + b"\x00" * (offset - len(existing))
note.write(existing[:offset] + data + existing[offset + len(data):])
note.write_content(existing[:offset] + data + existing[offset + len(data):])
return len(data)
async def release(self, fh: FileHandleT) -> None:
@ -380,12 +379,8 @@ class TroveFuseOps(pyfuse3.Operations):
async def unlink(self, parent_inode: InodeT, name: bytes, ctx) -> None:
parent_note = self._get_inode_note(parent_inode)
if not isinstance(parent_note, TroveTree):
raise pyfuse3.FUSEError(errno.ENOTDIR)
name_str = name.decode()
if name_str not in parent_note.list():
raise pyfuse3.FUSEError(errno.ENOENT)
parent_note.unlink(name.decode())
parent_note.rm_child(name_str, False)
async def rename(self, parent_inode_old: InodeT, name_old: bytes, parent_inode_new: InodeT, name_new: bytes, flags, ctx):
# Decode / validate names