Move away from inodes as direct db reference
This commit is contained in:
parent
f80f4d12a2
commit
e16d67e2f8
4 changed files with 375 additions and 154 deletions
|
|
@ -2,8 +2,8 @@ import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, List, Self
|
from typing import Optional, Dict, List, Self, Iterable
|
||||||
from .trove import NODE_ROOT_ID, Note, Trove, TreeNote, BlobNote, Blob, Tree, BadNoteType
|
from .trove import NODE_ROOT_ID, Note, Trove, TreeNote, BlobNote, Blob, Tree, BadNoteType, TreeEntry, NoteNotFound
|
||||||
|
|
||||||
|
|
||||||
class FSNote(Note):
|
class FSNote(Note):
|
||||||
|
|
@ -27,7 +27,9 @@ class FSNote(Note):
|
||||||
@property
|
@property
|
||||||
def _path(self) -> Path:
|
def _path(self) -> Path:
|
||||||
if self._fs_path is not None:
|
if self._fs_path is not None:
|
||||||
return self._fs_path
|
if self._fs_path.exists():
|
||||||
|
return self._fs_path
|
||||||
|
self._fs_path = None
|
||||||
if self._inode is None:
|
if self._inode is None:
|
||||||
raise ValueError("Note not yet saved to disk")
|
raise ValueError("Note not yet saved to disk")
|
||||||
self._fs_path = self._trove.get_path_by_inode(self._inode)
|
self._fs_path = self._trove.get_path_by_inode(self._inode)
|
||||||
|
|
@ -42,6 +44,8 @@ class FSNote(Note):
|
||||||
|
|
||||||
class FSBlobNote(FSNote, BlobNote):
|
class FSBlobNote(FSNote, BlobNote):
|
||||||
def read(self) -> bytes:
|
def read(self) -> bytes:
|
||||||
|
if self._inode is None:
|
||||||
|
return b""
|
||||||
return self._path.read_bytes()
|
return self._path.read_bytes()
|
||||||
|
|
||||||
def write(self, data: bytes) -> None:
|
def write(self, data: bytes) -> None:
|
||||||
|
|
@ -101,6 +105,17 @@ class FSTreeNote(FSNote, TreeNote):
|
||||||
self._trove._update_cache(inode, target_path)
|
self._trove._update_cache(inode, target_path)
|
||||||
return FSTreeNote(self._trove, inode=inode, path=target_path)
|
return FSTreeNote(self._trove, inode=inode, path=target_path)
|
||||||
|
|
||||||
|
def entries(self) -> Iterable[TreeEntry]:
|
||||||
|
try:
|
||||||
|
for item in self._path.iterdir():
|
||||||
|
if item.name == ".trove":
|
||||||
|
continue
|
||||||
|
inode = item.stat().st_ino
|
||||||
|
self._trove._update_cache(inode, item)
|
||||||
|
yield TreeEntry(name=item.name, object_id=inode)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
def list(self) -> dict[str, int]:
|
def list(self) -> dict[str, int]:
|
||||||
res = {}
|
res = {}
|
||||||
try:
|
try:
|
||||||
|
|
@ -113,9 +128,16 @@ class FSTreeNote(FSNote, TreeNote):
|
||||||
pass
|
pass
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def child(self, name: str) -> Note:
|
||||||
|
"""Retrieve a child not by name."""
|
||||||
|
target_path = self._path / name
|
||||||
|
return self._trove.get_raw_note_by_path(target_path)
|
||||||
|
|
||||||
|
|
||||||
class FSTrove(Trove):
|
class FSTrove(Trove):
|
||||||
def __init__(self, root: str | Path):
|
def __init__(self, root: str | Path):
|
||||||
self.root = Path(root).absolute()
|
self.root = Path(root).absolute()
|
||||||
|
self._root_inode = self.root.stat().st_ino
|
||||||
self.dot_trove = self.root / ".trove"
|
self.dot_trove = self.root / ".trove"
|
||||||
self.working = self.dot_trove / ".working"
|
self.working = self.dot_trove / ".working"
|
||||||
|
|
||||||
|
|
@ -160,7 +182,7 @@ class FSTrove(Trove):
|
||||||
self.con.commit()
|
self.con.commit()
|
||||||
|
|
||||||
def get_path_by_inode(self, inode: int) -> Optional[Path]:
|
def get_path_by_inode(self, inode: int) -> Optional[Path]:
|
||||||
if inode == NODE_ROOT_ID:
|
if inode == self._root_inode:
|
||||||
return self.root
|
return self.root
|
||||||
|
|
||||||
row = self.con.execute("SELECT path FROM cache WHERE inode = ?", (inode,)).fetchone()
|
row = self.con.execute("SELECT path FROM cache WHERE inode = ?", (inode,)).fetchone()
|
||||||
|
|
@ -189,14 +211,21 @@ class FSTrove(Trove):
|
||||||
continue
|
continue
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_raw_note(self, note_id: int) -> Optional[Note]:
|
def get_raw_note_by_path(self, target_path: Path) -> Note:
|
||||||
|
if not target_path.exists():
|
||||||
|
raise NoteNotFound(target_path.relative_to(self.root))
|
||||||
|
note_id = target_path.stat().st_ino
|
||||||
|
if target_path.is_dir():
|
||||||
|
return FSTreeNote(self, inode=note_id, path=target_path)
|
||||||
|
else:
|
||||||
|
return FSBlobNote(self, inode=note_id, path=target_path)
|
||||||
|
|
||||||
|
def get_raw_note(self, note_id: int) -> Note:
|
||||||
p = self.get_path_by_inode(note_id)
|
p = self.get_path_by_inode(note_id)
|
||||||
if not p:
|
if not p:
|
||||||
return None
|
raise NoteNotFound(note_id)
|
||||||
if p.is_dir():
|
return self.get_raw_note_by_path(p)
|
||||||
return FSTreeNote(self, inode=note_id, path=p)
|
|
||||||
else:
|
|
||||||
return FSBlobNote(self, inode=note_id, path=p)
|
|
||||||
|
|
||||||
def create_blob(self, data: bytes | None = None) -> BlobNote:
|
def create_blob(self, data: bytes | None = None) -> BlobNote:
|
||||||
fd, temp_path = tempfile.mkstemp(dir=self.working)
|
fd, temp_path = tempfile.mkstemp(dir=self.working)
|
||||||
|
|
@ -211,7 +240,7 @@ class FSTrove(Trove):
|
||||||
return FSBlobNote(self, inode=inode, path=p)
|
return FSBlobNote(self, inode=inode, path=p)
|
||||||
|
|
||||||
def get_root(self) -> TreeNote:
|
def get_root(self) -> TreeNote:
|
||||||
return FSTreeNote(self, inode=NODE_ROOT_ID, path=self.root)
|
return FSTreeNote(self, inode=self._root_inode, path=self.root)
|
||||||
|
|
||||||
def _get_metadata(self, inode: int, key: str) -> Optional[bytes]:
|
def _get_metadata(self, inode: int, key: str) -> Optional[bytes]:
|
||||||
row = self.con.execute("SELECT value FROM metadata WHERE inode = ? AND key = ?", (inode, key)).fetchone()
|
row = self.con.execute("SELECT value FROM metadata WHERE inode = ? AND key = ?", (inode, key)).fetchone()
|
||||||
|
|
|
||||||
|
|
@ -13,34 +13,184 @@ import errno
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import Sequence, Tuple, cast
|
||||||
|
|
||||||
import pyfuse3
|
import pyfuse3
|
||||||
import trio
|
import trio
|
||||||
|
from pyfuse3 import InodeT, FileHandleT
|
||||||
|
|
||||||
from trovedb.trove import Trove, Note, Tree as TroveTree, TreeNote, Blob as TroveBlob
|
from trovedb.trove import Trove, Note, Tree as TroveTree, TreeNote, Blob as TroveBlob, ObjectId, TreeExists
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class _TroveEntry:
|
||||||
|
__slots__ = [ 'object_id', 'sys_inode', 'ref_count' ]
|
||||||
|
def __init__(self, sys_inode: InodeT, object_id: ObjectId | None):
|
||||||
|
self.object_id: ObjectId | None = object_id
|
||||||
|
self.sys_inode: InodeT = sys_inode
|
||||||
|
self.ref_count = 0
|
||||||
|
|
||||||
|
def ref(self) -> None:
|
||||||
|
self.ref_count += 1
|
||||||
|
|
||||||
|
def deref(self, count: int = 1) -> bool:
|
||||||
|
assert self.ref_count > 0
|
||||||
|
self.ref_count -= count
|
||||||
|
return self.ref_count <= 0
|
||||||
|
|
||||||
|
class _TroveHandle:
|
||||||
|
__slots__ = [ 'inode_id', 'ref_count', 'note', 'handle_id' ]
|
||||||
|
def __init__(self, inode_id: InodeT, handle_id: FileHandleT, note: Note):
|
||||||
|
self.inode_id = inode_id
|
||||||
|
self.handle_id = handle_id
|
||||||
|
self.note = note
|
||||||
|
|
||||||
|
class _TroveHandleTree(_TroveHandle):
|
||||||
|
@property
|
||||||
|
def tree(self) -> TreeNote:
|
||||||
|
return cast(TreeNote, self.note)
|
||||||
|
|
||||||
|
|
||||||
class TroveFuseOps(pyfuse3.Operations):
|
class TroveFuseOps(pyfuse3.Operations):
|
||||||
|
|
||||||
enable_writeback_cache = False
|
|
||||||
|
|
||||||
def __init__(self, trove: Trove):
|
def __init__(self, trove: Trove):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._trove = trove
|
self._trove = trove
|
||||||
|
|
||||||
|
# Inode Cache
|
||||||
|
self._next_inode = 2
|
||||||
|
self._inode_cache: dict[InodeT, _TroveEntry] = {}
|
||||||
|
self._inode_reverse_cache: dict[ObjectId, InodeT] = {}
|
||||||
|
|
||||||
|
# Cache and Lock Root Inode
|
||||||
|
node_root = trove.get_root()
|
||||||
|
self._inode_cache[pyfuse3.ROOT_INODE] = _TroveEntry(pyfuse3.ROOT_INODE, node_root.object_id)
|
||||||
|
self._inode_reverse_cache[node_root.object_id] = pyfuse3.ROOT_INODE
|
||||||
|
self._inode_cache[pyfuse3.ROOT_INODE].ref()
|
||||||
|
self._inode_cache[pyfuse3.ROOT_INODE].ref()
|
||||||
|
|
||||||
|
# Handles
|
||||||
|
self._next_handle = 2
|
||||||
|
self._handles: dict[int, _TroveHandle] = {}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Helpers
|
# Entry Management [entries relate inode to Note]
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _note_or_error(self, inode: int):
|
def _get_ent_from_inode(self, inode: InodeT) -> _TroveEntry:
|
||||||
note = self._trove.get_raw_note(inode)
|
"""Get entry from predefined inode"""
|
||||||
|
if inode not in self._inode_cache:
|
||||||
|
logger.debug("inode not found in cache: %d", inode)
|
||||||
|
raise pyfuse3.FUSEError(errno.ENOENT)
|
||||||
|
value = self._inode_cache[inode]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _create_get_ent_from_note(self, note: Note) -> _TroveEntry:
|
||||||
|
"""Create entry from note. Inode is reserved but not saved in cache."""
|
||||||
|
if note.object_id in self._inode_reverse_cache:
|
||||||
|
sys_inode = self._inode_reverse_cache[note.object_id]
|
||||||
|
return self._inode_cache[sys_inode]
|
||||||
|
sys_inode = InodeT(self._next_inode)
|
||||||
|
self._next_inode += 1
|
||||||
|
return _TroveEntry(sys_inode=sys_inode, object_id=note.object_id)
|
||||||
|
|
||||||
|
def _ref_entry(self, ent: _TroveEntry) -> None:
|
||||||
|
"""Ref entry. If it is not in cache, it is added to cache."""
|
||||||
|
if ent.sys_inode not in self._inode_cache:
|
||||||
|
self._inode_cache[ent.sys_inode] = ent
|
||||||
|
self._inode_reverse_cache[ent.object_id] = ent.sys_inode
|
||||||
|
ent.ref()
|
||||||
|
|
||||||
|
def _deref_entry(self, ent: _TroveEntry, count: int = 1) -> None:
|
||||||
|
"""Deref entry. Remove from cache if count hits 0"""
|
||||||
|
if ent.deref(count):
|
||||||
|
if ent.sys_inode in self._inode_cache:
|
||||||
|
logger.debug("free inode: %d", ent.sys_inode)
|
||||||
|
del self._inode_cache[ent.sys_inode]
|
||||||
|
del self._inode_reverse_cache[ent.object_id]
|
||||||
|
|
||||||
|
def _get_inode_note(self, inode: InodeT) -> Note:
|
||||||
|
"""Get note from Inode, inode must be reserved"""
|
||||||
|
ent = self._get_ent_from_inode(inode)
|
||||||
|
return self._get_ent_note(ent)
|
||||||
|
|
||||||
|
def _get_ent_note(self, ent: _TroveEntry) -> Note:
|
||||||
|
"""Get note from entry."""
|
||||||
|
note = self._trove.get_raw_note(ent.object_id)
|
||||||
if note is None:
|
if note is None:
|
||||||
|
logger.debug("note lookup failed: %s", ent.object_id)
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
raise pyfuse3.FUSEError(errno.ENOENT)
|
||||||
return note
|
return note
|
||||||
|
|
||||||
def _make_attr(self, inode: int, is_tree: bool, size: int = 0) -> pyfuse3.EntryAttributes:
|
def _lookup_update_object(self, object_id: ObjectId) -> _TroveEntry:
|
||||||
|
if object_id in self._inode_reverse_cache:
|
||||||
|
inode = self._inode_reverse_cache[object_id]
|
||||||
|
return self._lookup_existing(inode)
|
||||||
|
else:
|
||||||
|
inode_id = InodeT(self._next_inode)
|
||||||
|
self._next_inode += 1
|
||||||
|
inode = _TroveEntry(sys_inode=inode_id, object_id=object_id)
|
||||||
|
self._inode_cache[inode_id] = inode
|
||||||
|
self._inode_reverse_cache[object_id] = inode_id
|
||||||
|
return inode
|
||||||
|
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
return ent, note
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sys_inode_id(self, object_id: ObjectId) -> InodeT:
|
||||||
|
if object_id in self._inode_reverse_cache:
|
||||||
|
return self._inode_reverse_cache[object_id]
|
||||||
|
else:
|
||||||
|
raise pyfuse3.FUSEError(errno.ENOENT)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Handle Management
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _open_handle(self, inode: InodeT) -> _TroveHandle:
|
||||||
|
note = self._get_inode_note(inode)
|
||||||
|
|
||||||
|
handle_id = FileHandleT(self._next_handle)
|
||||||
|
self._next_handle += 1
|
||||||
|
|
||||||
|
handle: _TroveHandle
|
||||||
|
if isinstance(note, TreeNote):
|
||||||
|
handle = _TroveHandleTree(inode_id=inode, handle_id=handle_id, note=note)
|
||||||
|
else:
|
||||||
|
handle = _TroveHandle(inode_id=inode, handle_id=handle_id, note=note)
|
||||||
|
|
||||||
|
self._handles[handle_id] = handle
|
||||||
|
return handle
|
||||||
|
|
||||||
|
def _get_handle(self, handle_id: FileHandleT) -> _TroveHandle:
|
||||||
|
if not handle_id in self._handles:
|
||||||
|
raise pyfuse3.FUSEError(errno.EBADF)
|
||||||
|
return self._handles[handle_id]
|
||||||
|
|
||||||
|
def _close_handle(self, handle: _TroveHandle):
|
||||||
|
del self._handles[handle.handle_id]
|
||||||
|
|
||||||
|
def _get_attr(self, ent: _TroveEntry, note: Note) -> pyfuse3.EntryAttributes:
|
||||||
|
# Determine basic information
|
||||||
|
is_tree = True
|
||||||
|
size = 0
|
||||||
|
if isinstance(note, TroveBlob):
|
||||||
|
size = len(note.read())
|
||||||
|
is_tree = False
|
||||||
|
|
||||||
|
# Create and fill attr structure
|
||||||
attr = pyfuse3.EntryAttributes()
|
attr = pyfuse3.EntryAttributes()
|
||||||
attr.st_ino = pyfuse3.InodeT(inode)
|
attr.st_ino = ent.sys_inode
|
||||||
attr.st_nlink = 1
|
attr.st_nlink = 1
|
||||||
attr.st_uid = os.getuid()
|
attr.st_uid = os.getuid()
|
||||||
attr.st_gid = os.getgid()
|
attr.st_gid = os.getgid()
|
||||||
|
|
@ -63,116 +213,128 @@ class TroveFuseOps(pyfuse3.Operations):
|
||||||
attr.st_blocks = (size + 511) // 512
|
attr.st_blocks = (size + 511) // 512
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
def _attr_for_note(self, note: Note) -> pyfuse3.EntryAttributes:
|
|
||||||
size = 0
|
|
||||||
is_tree = True
|
|
||||||
if isinstance(note, TroveBlob):
|
|
||||||
size = len(note.read())
|
|
||||||
is_tree = False
|
|
||||||
return self._make_attr(note.object_id, is_tree, size)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Stat / lookup
|
# Stat / lookup
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def getattr(self, inode: int, ctx=None) -> pyfuse3.EntryAttributes:
|
async def getattr(self, inode: InodeT, ctx=None) -> pyfuse3.EntryAttributes:
|
||||||
note = self._note_or_error(inode)
|
logger.debug("getattr inode:%d", inode)
|
||||||
return self._attr_for_note(note)
|
ent = self._get_ent_from_inode(inode)
|
||||||
|
note = self._get_ent_note(ent)
|
||||||
|
return self._get_attr(ent, note)
|
||||||
|
|
||||||
async def lookup(self, parent_inode: int, name: bytes, ctx=None) -> pyfuse3.EntryAttributes:
|
async def lookup(self, parent_inode: InodeT, name: bytes, ctx=None) -> pyfuse3.EntryAttributes:
|
||||||
parent = self._note_or_error(parent_inode)
|
logger.debug("lookup inode:%d name:%s", parent_inode, name)
|
||||||
if not isinstance(parent, TroveTree):
|
ent, child = self._lookup_child(parent_inode, name)
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
self._ref_entry(ent)
|
||||||
entries = parent.list()
|
return self._get_attr(ent, child)
|
||||||
name_str = name.decode()
|
|
||||||
if name_str not in entries:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
child = self._trove.get_raw_note(entries[name_str])
|
|
||||||
if child is None:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
return self._attr_for_note(child)
|
|
||||||
|
|
||||||
async def setattr(self, inode: int, attr, fields, fh, ctx) -> pyfuse3.EntryAttributes:
|
async def setattr(self, inode: InodeT, attr, fields, fh: FileHandleT | None, ctx) -> pyfuse3.EntryAttributes:
|
||||||
note = self._note_or_error(inode)
|
ent = self._get_ent_from_inode(inode)
|
||||||
if fields.update_size and not isinstance(note, TroveTree):
|
note = self._get_ent_note(ent)
|
||||||
current = note.read()
|
if fields.update_size:
|
||||||
new_size = attr.st_size
|
if isinstance(note, TroveBlob):
|
||||||
if new_size < len(current):
|
current = note.read()
|
||||||
note.write(current[:new_size])
|
new_size = attr.st_size
|
||||||
elif new_size > len(current):
|
if new_size < len(current):
|
||||||
note.write(current + b"\x00" * (new_size - len(current)))
|
note.write(current[:new_size])
|
||||||
return self._attr_for_note(note)
|
elif new_size > len(current):
|
||||||
|
note.write(current + b"\x00" * (new_size - len(current)))
|
||||||
|
else:
|
||||||
|
raise pyfuse3.FUSEError(errno.EINVAL)
|
||||||
|
return self._get_attr(ent, note)
|
||||||
|
|
||||||
async def forget(self, inode_list) -> None:
|
def forget(self, inode_list: Sequence[Tuple[InodeT, int]]) -> None:
|
||||||
pass
|
for inode, nlookup in inode_list:
|
||||||
|
try:
|
||||||
|
logger.debug("deref inode:%d count:%d", inode, nlookup)
|
||||||
|
self._deref_entry(self._get_ent_from_inode(inode), nlookup)
|
||||||
|
except pyfuse3.FUSEError as e:
|
||||||
|
logger.warning("Failed to deref inode %d: %s", inode, str(e))
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Directory ops
|
# Directory ops
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def opendir(self, inode: int, ctx) -> pyfuse3.FileHandleT:
|
async def opendir(self, inode: InodeT, ctx) -> FileHandleT:
|
||||||
note = self._note_or_error(inode)
|
handle = self._open_handle(inode)
|
||||||
if not isinstance(note, TroveTree):
|
if not isinstance(handle, _TroveHandleTree):
|
||||||
|
logger.debug("attempted opendir on %d not a tree", inode)
|
||||||
|
self._close_handle(handle)
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
return pyfuse3.FileHandleT(inode)
|
logger.debug("opened dir inode %d -> handle %d", inode, handle.handle_id)
|
||||||
|
return handle.handle_id
|
||||||
|
|
||||||
async def readdir(self, fh: int, start_id: int, token) -> None:
|
async def readdir(self, fh: FileHandleT, start_id: int, token) -> None:
|
||||||
note = self._note_or_error(fh)
|
logger.debug("readdir %d start_id %d", fh, start_id)
|
||||||
|
handle = self._get_handle(fh)
|
||||||
|
note = handle.note
|
||||||
if not isinstance(note, TroveTree):
|
if not isinstance(note, TroveTree):
|
||||||
|
logger.debug("attempted readdir on %d not a tree", fh)
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
entries = list(note.list().items()) # [(name, object_id), ...]
|
entries = list(note.list().items()) # [(name, object_id), ...]
|
||||||
|
|
||||||
for idx, (name, child_id) in enumerate(entries):
|
for idx, (name, child_id) in enumerate(entries):
|
||||||
if idx < start_id:
|
if idx < start_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
child = self._trove.get_raw_note(child_id)
|
child = self._trove.get_raw_note(child_id)
|
||||||
if child is None:
|
if child is None:
|
||||||
continue
|
continue
|
||||||
attr = self._attr_for_note(child)
|
|
||||||
|
child_ent = self._create_get_ent_from_note(child)
|
||||||
|
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, name.encode(), attr, idx + 1):
|
||||||
break
|
break
|
||||||
|
|
||||||
async def releasedir(self, fh: int) -> None:
|
async def releasedir(self, fh: FileHandleT) -> None:
|
||||||
pass
|
logger.debug("releasedir %d", fh)
|
||||||
|
handle = self._get_handle(fh)
|
||||||
|
self._close_handle(handle)
|
||||||
|
|
||||||
async def mkdir(self, parent_inode: int, name: bytes, mode: int, ctx) -> pyfuse3.EntryAttributes:
|
async def mkdir(self, parent_inode: InodeT, name: bytes, mode: int, ctx) -> pyfuse3.EntryAttributes:
|
||||||
parent = self._note_or_error(parent_inode)
|
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):
|
if not isinstance(parent, TreeNote):
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
name_str = name.decode()
|
# Create new directory in note
|
||||||
if name_str in parent.list():
|
try:
|
||||||
raise pyfuse3.FUSEError(errno.EEXIST)
|
new_tree: TreeNote = parent.mkdir(name.decode())
|
||||||
new_tree: TreeNote = parent.mkdir(name_str)
|
except TreeExists:
|
||||||
return self._make_attr(new_tree.object_id, True, 0)
|
raise pyfuse3.FUSEError(errno.EEXIST) from None
|
||||||
|
# Grab entity for kernel
|
||||||
|
ent = self._create_get_ent_from_note(new_tree)
|
||||||
|
self._ref_entry(ent)
|
||||||
|
return self._get_attr(ent, new_tree)
|
||||||
|
|
||||||
async def rmdir(self, parent_inode: int, name: bytes, ctx) -> None:
|
async def rmdir(self, parent_inode: InodeT, name: bytes, ctx) -> None:
|
||||||
parent = self._note_or_error(parent_inode)
|
logger.debug("rmdir inode:%d name:%s", parent_inode, name)
|
||||||
if not isinstance(parent, TroveTree):
|
parent = self._get_inode_note(parent_inode)
|
||||||
|
if not isinstance(parent, TreeNote):
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
name_str = name.decode()
|
try:
|
||||||
entries = parent.list()
|
parent.unlink(name.decode())
|
||||||
if name_str not in entries:
|
except KeyError:
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
raise pyfuse3.FUSEError(errno.ENOENT) from None
|
||||||
child = self._trove.get_raw_note(entries[name_str])
|
|
||||||
if child is None:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
if not isinstance(child, TroveTree):
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
|
||||||
if child.list():
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOTEMPTY)
|
|
||||||
parent.unlink(name_str)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# File ops
|
# File ops
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def open(self, inode: int, flags, ctx) -> pyfuse3.FileInfo:
|
async def open(self, inode: InodeT, flags, ctx) -> pyfuse3.FileInfo:
|
||||||
note = self._note_or_error(inode)
|
handle = self._open_handle(inode)
|
||||||
if isinstance(note, TroveTree):
|
if isinstance(handle.note, TroveTree):
|
||||||
|
self._close_handle(handle)
|
||||||
raise pyfuse3.FUSEError(errno.EISDIR)
|
raise pyfuse3.FUSEError(errno.EISDIR)
|
||||||
return pyfuse3.FileInfo(fh=pyfuse3.FileHandleT(inode))
|
return pyfuse3.FileInfo(fh=handle.handle_id)
|
||||||
|
|
||||||
async def create(self, parent_inode: int, name: bytes, mode: int, flags, ctx) -> tuple:
|
async def create(self, parent_inode: InodeT, name: bytes, mode: int, flags, ctx) -> tuple:
|
||||||
parent = self._note_or_error(parent_inode)
|
logger.debug("create inode:%d name:%s", parent_inode, name)
|
||||||
|
parent = self._get_inode_note(parent_inode)
|
||||||
if not isinstance(parent, TroveTree):
|
if not isinstance(parent, TroveTree):
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
name_str = name.decode()
|
name_str = name.decode()
|
||||||
|
|
@ -180,67 +342,66 @@ class TroveFuseOps(pyfuse3.Operations):
|
||||||
raise pyfuse3.FUSEError(errno.EEXIST)
|
raise pyfuse3.FUSEError(errno.EEXIST)
|
||||||
blob = self._trove.create_blob(b"")
|
blob = self._trove.create_blob(b"")
|
||||||
parent.link(name_str, blob)
|
parent.link(name_str, blob)
|
||||||
attr = self._make_attr(blob.object_id, False, 0)
|
|
||||||
return pyfuse3.FileInfo(fh=pyfuse3.FileHandleT(blob.object_id)), attr
|
|
||||||
|
|
||||||
async def read(self, fh: int, offset: int, length: int) -> bytes:
|
ent = self._create_get_ent_from_note(blob)
|
||||||
note = self._note_or_error(fh)
|
self._ref_entry(ent)
|
||||||
return note.read()[offset:offset + length]
|
|
||||||
|
|
||||||
async def write(self, fh: int, offset: int, data: bytes) -> int:
|
handle = self._open_handle(ent.sys_inode)
|
||||||
note = self._note_or_error(fh)
|
attr = self._get_attr(ent, blob)
|
||||||
existing = note.read()
|
return pyfuse3.FileInfo(fh=handle.handle_id), attr
|
||||||
if offset > len(existing):
|
|
||||||
existing = existing + b"\x00" * (offset - len(existing))
|
async def read(self, fh: FileHandleT, offset: int, length: int) -> bytes:
|
||||||
note.write(existing[:offset] + data + existing[offset + len(data):])
|
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]
|
||||||
|
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 offset > len(existing):
|
||||||
|
existing = existing + b"\x00" * (offset - len(existing))
|
||||||
|
note.write(existing[:offset] + data + existing[offset + len(data):])
|
||||||
return len(data)
|
return len(data)
|
||||||
|
|
||||||
async def release(self, fh: int) -> None:
|
async def release(self, fh: FileHandleT) -> None:
|
||||||
pass
|
handle = self._get_handle(fh)
|
||||||
|
self._close_handle(handle)
|
||||||
|
|
||||||
async def unlink(self, parent_inode: int, name: bytes, ctx) -> None:
|
async def unlink(self, parent_inode: InodeT, name: bytes, ctx) -> None:
|
||||||
parent = self._note_or_error(parent_inode)
|
parent_note = self._get_inode_note(parent_inode)
|
||||||
if not isinstance(parent, TroveTree):
|
if not isinstance(parent_note, TroveTree):
|
||||||
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
name_str = name.decode()
|
name_str = name.decode()
|
||||||
entries = parent.list()
|
if name_str not in parent_note.list():
|
||||||
if name_str not in entries:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
raise pyfuse3.FUSEError(errno.ENOENT)
|
||||||
child = self._trove.get_raw_note(entries[name_str])
|
parent_note.unlink(name.decode())
|
||||||
if child is None:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
if isinstance(child, TroveTree):
|
|
||||||
raise pyfuse3.FUSEError(errno.EISDIR)
|
|
||||||
parent.unlink(name_str)
|
|
||||||
|
|
||||||
async def rename(self, parent_inode_old, name_old, parent_inode_new, name_new, flags, ctx):
|
async def rename(self, parent_inode_old: InodeT, name_old: bytes, parent_inode_new: InodeT, name_new: bytes, flags, ctx):
|
||||||
old_parent = self._note_or_error(parent_inode_old)
|
# Decode / validate names
|
||||||
new_parent = self._note_or_error(parent_inode_new)
|
name_new_str = name_new.decode()
|
||||||
if not isinstance(old_parent, TroveTree) or not isinstance(new_parent, TroveTree):
|
name_old_str = name_old.decode()
|
||||||
|
|
||||||
|
# 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)
|
raise pyfuse3.FUSEError(errno.ENOTDIR)
|
||||||
|
|
||||||
name_old_str = name_old.decode()
|
# We want to maintain the inode - find the note via the internal entity
|
||||||
name_new_str = name_new.decode()
|
ent, note = self._lookup_child(parent_inode_old, name_old)
|
||||||
|
|
||||||
old_entries = old_parent.list()
|
# Remove existing target
|
||||||
if name_old_str not in old_entries:
|
new_parent.unlink(name_new_str)
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
|
|
||||||
child_id = old_entries[name_old_str]
|
# Link to new parent, unlink from old
|
||||||
child = self._trove.get_raw_note(child_id)
|
new_parent.link(name_new_str, note)
|
||||||
if child is None:
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOENT)
|
|
||||||
|
|
||||||
# Remove existing target if present
|
|
||||||
new_entries = new_parent.list()
|
|
||||||
if name_new_str in new_entries:
|
|
||||||
target = self._trove.get_raw_note(new_entries[name_new_str])
|
|
||||||
if target is not None and isinstance(target, TroveTree):
|
|
||||||
if target.list():
|
|
||||||
raise pyfuse3.FUSEError(errno.ENOTEMPTY)
|
|
||||||
new_parent.unlink(name_new_str)
|
|
||||||
|
|
||||||
new_parent.link(name_new_str, child)
|
|
||||||
old_parent.unlink(name_old_str)
|
old_parent.unlink(name_old_str)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -249,6 +410,8 @@ class TroveFuseOps(pyfuse3.Operations):
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def _run(ops: TroveFuseOps, mountpoint: str) -> None:
|
async def _run(ops: TroveFuseOps, mountpoint: str) -> None:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
options = set(pyfuse3.default_options)
|
options = set(pyfuse3.default_options)
|
||||||
options.add("fsname=trove")
|
options.add("fsname=trove")
|
||||||
pyfuse3.init(ops, mountpoint, options)
|
pyfuse3.init(ops, mountpoint, options)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
from typing import Protocol, runtime_checkable, Optional, Dict, List, Self
|
from typing import Protocol, runtime_checkable, Optional, Dict, List, Self, NamedTuple, Iterable, MappingView
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
|
|
||||||
NODE_ROOT_ID = 1
|
|
||||||
|
type ObjectId = int
|
||||||
|
|
||||||
|
NODE_ROOT_ID: ObjectId = 1
|
||||||
|
|
||||||
class BadNoteType(TypeError):
|
class BadNoteType(TypeError):
|
||||||
"""Raised when an invalid note type is encountered."""
|
"""Raised when an invalid note type is encountered."""
|
||||||
|
|
||||||
|
class TreeExists(TypeError):
|
||||||
|
"""Raised when a label already exists."""
|
||||||
|
|
||||||
|
class NoteNotFound(KeyError):
|
||||||
|
"""Raised when a note is not found."""
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Note(Protocol):
|
class Note(Protocol):
|
||||||
|
|
@ -15,7 +23,7 @@ class Note(Protocol):
|
||||||
Represents access to an individual note's content and metadata.
|
Represents access to an individual note's content and metadata.
|
||||||
"""
|
"""
|
||||||
@property
|
@property
|
||||||
def object_id(self) -> int:
|
def object_id(self) -> ObjectId:
|
||||||
"""The unique identifier for this note."""
|
"""The unique identifier for this note."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
@ -37,6 +45,11 @@ class Blob(Protocol):
|
||||||
"""Write new content to the note."""
|
"""Write new content to the note."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class TreeEntry(NamedTuple):
|
||||||
|
name: str
|
||||||
|
object_id: ObjectId
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class Tree(Protocol):
|
class Tree(Protocol):
|
||||||
def link(self, name: str, note: Note):
|
def link(self, name: str, note: Note):
|
||||||
|
|
@ -56,7 +69,11 @@ class Tree(Protocol):
|
||||||
...
|
...
|
||||||
|
|
||||||
def child(self, name: str) -> Note:
|
def child(self, name: str) -> Note:
|
||||||
"""Retrieve a child not by name."""
|
"""Retrieve a child note by name."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def entries(self) -> Iterable[TreeEntry]:
|
||||||
|
"""Return all entries in the directory"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def list(self) -> dict[str, int]:
|
def list(self) -> dict[str, int]:
|
||||||
|
|
@ -78,8 +95,8 @@ class Trove(Protocol):
|
||||||
Provides high-level access to notes and trees.
|
Provides high-level access to notes and trees.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_raw_note(self, note: int) -> Optional[Note]:
|
def get_raw_note(self, note: ObjectId) -> Note:
|
||||||
"""Retrieve a note by a UUID"""
|
"""Retrieve a note by a object id"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def create_blob(self, data: bytes | None = None) -> BlobNote:
|
def create_blob(self, data: bytes | None = None) -> BlobNote:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from .db import Sqlite3Trove
|
from .db import Sqlite3Trove
|
||||||
from .tree import Tree as TreeData
|
from .tree import Tree as TreeData
|
||||||
from .trove import NODE_ROOT_ID, Note, Trove, TreeNote, BlobNote
|
from .trove import NODE_ROOT_ID, Note, Trove, TreeNote, BlobNote, TreeEntry, NoteNotFound
|
||||||
|
|
||||||
|
|
||||||
class NoteImpl(Note):
|
class NoteImpl(Note):
|
||||||
|
|
@ -65,9 +65,12 @@ class TreeNoteImpl(NoteImpl, TreeNote):
|
||||||
|
|
||||||
def unlink(self, name: str) -> None:
|
def unlink(self, name: str) -> None:
|
||||||
"""Remove an entry by name. Raises KeyError if not found."""
|
"""Remove an entry by name. Raises KeyError if not found."""
|
||||||
tree = self._read_tree()
|
try:
|
||||||
tree.rm_entry(name)
|
tree = self._read_tree()
|
||||||
self._flush_tree(tree)
|
tree.rm_entry(name)
|
||||||
|
self._flush_tree(tree)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
def mkdir(self, name: str) -> 'TreeNoteImpl':
|
def mkdir(self, name: str) -> 'TreeNoteImpl':
|
||||||
"""Create a new empty tree, link it under name, and return it."""
|
"""Create a new empty tree, link it under name, and return it."""
|
||||||
|
|
@ -91,7 +94,16 @@ class TreeNoteImpl(NoteImpl, TreeNote):
|
||||||
if name not in entries:
|
if name not in entries:
|
||||||
raise KeyError(f"Entry '{name}' not found")
|
raise KeyError(f"Entry '{name}' not found")
|
||||||
child_id = entries[name]
|
child_id = entries[name]
|
||||||
return self._parent.get_raw_note(child_id)
|
value = self._parent.get_raw_note(child_id)
|
||||||
|
if value is None:
|
||||||
|
raise KeyError(f"Entry '{name}' has no value")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def entries(self):
|
||||||
|
"""Return all entries as an iterable of TreeEntry."""
|
||||||
|
tree = self._read_tree()
|
||||||
|
for name, object_id in tree.list().items():
|
||||||
|
yield TreeEntry(name, object_id)
|
||||||
|
|
||||||
def list(self) -> dict[str, int]:
|
def list(self) -> dict[str, int]:
|
||||||
"""Return all entries as {name: object_id}."""
|
"""Return all entries as {name: object_id}."""
|
||||||
|
|
@ -138,11 +150,11 @@ class TroveImpl:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
# Trove protocol
|
# Trove protocol
|
||||||
def get_raw_note(self, note_id: int) -> Optional[Note]:
|
def get_raw_note(self, note_id: int) -> Note:
|
||||||
"""Return a BlobNote or TreeNote for the given id, or None if not found."""
|
"""Return a BlobNote or TreeNote for the given id, or None if not found."""
|
||||||
ot = self._db.get_object_type(note_id)
|
ot = self._db.get_object_type(note_id)
|
||||||
if ot is None:
|
if ot is None:
|
||||||
return None
|
raise NoteNotFound(note_id)
|
||||||
if ot == "blob":
|
if ot == "blob":
|
||||||
return BlobNoteImpl(self, note_id)
|
return BlobNoteImpl(self, note_id)
|
||||||
if ot == "tree":
|
if ot == "tree":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue