Add more meta information to trove entries

This commit is contained in:
Andrew Mulbrook 2026-03-22 23:57:05 -05:00
parent 4ada169bbe
commit bd4d571c95
5 changed files with 80 additions and 7 deletions

View file

@ -71,7 +71,7 @@ class Sqlite3Trove:
con.commit() con.commit()
obj = cls(con) obj = cls(con)
if initialize: if initialize:
obj.write_blob(b"", NODE_ROOT_ID) obj.write_blob(b"", NOTE_ROOT_ID)
return obj return obj
def close(self): def close(self):
@ -103,6 +103,15 @@ class Sqlite3Trove:
return None return None
return bytes(row["data"]) if row["data"] is not None else b"" return bytes(row["data"]) if row["data"] is not None else b""
def get_mtime(self, object_id: int) -> datetime | None:
"""Return the modified timestamp for an object, or None if not found."""
row = self._con.execute(
"SELECT modified FROM objects WHERE id = ?", (object_id,)
).fetchone()
if row is None:
return None
return datetime.fromisoformat(row["modified"])
def read_metadata(self, object_id: int, key: str) -> bytes | None: def read_metadata(self, object_id: int, key: str) -> bytes | None:
"""Return raw metadata value for (uuid, key), or None if not found.""" """Return raw metadata value for (uuid, key), or None if not found."""
row = self._con.execute( row = self._con.execute(

View file

@ -1,6 +1,7 @@
import os import os
import sqlite3 import sqlite3
import tempfile import tempfile
import datetime as dt
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, List, Self, Iterable from typing import Optional, Dict, List, Self, Iterable
from .trove import Note, Trove, TreeNote, BlobNote, Blob, Tree, BadNoteType, TreeEntry, NoteNotFound from .trove import Note, Trove, TreeNote, BlobNote, Blob, Tree, BadNoteType, TreeEntry, NoteNotFound
@ -24,6 +25,24 @@ class FSNote(Note):
raise ValueError("Note not yet saved to disk") raise ValueError("Note not yet saved to disk")
return self._inode return self._inode
@property
def mtime(self):
"""Return modification time as datetime."""
stat = self._path.stat()
return dt.datetime.fromtimestamp(stat.st_mtime, tz=dt.timezone.utc)
@property
def readonly(self) -> bool:
"""Check if the note is readonly based on file permissions."""
if self._inode is None:
return False
return not os.access(self._path, os.W_OK)
@property
def mime(self) -> str:
"""Return MIME type, defaulting to generic binary stream."""
return "application/octet-stream"
@property @property
def _path(self) -> Path: def _path(self) -> Path:
if self._fs_path is not None: if self._fs_path is not None:
@ -60,6 +79,11 @@ class FSBlobNote(FSNote, BlobNote):
pass pass
class FSTreeNote(FSNote, TreeNote): class FSTreeNote(FSNote, TreeNote):
@property
def mime(self) -> str:
"""Return MIME type for directory/tree nodes."""
return "inode/directory"
def link(self, name: str, note: Note): def link(self, name: str, note: Note):
if not isinstance(note, FSBlobNote): if not isinstance(note, FSBlobNote):
raise BadNoteType("Only blob notes can be linked") raise BadNoteType("Only blob notes can be linked")

View file

@ -194,20 +194,26 @@ class TroveFuseOps(pyfuse3.Operations):
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()
mtime_ns = int(note.mtime.timestamp() * 1e9)
now_ns = int(time.time() * 1e9) now_ns = int(time.time() * 1e9)
attr.st_atime_ns = now_ns attr.st_atime_ns = now_ns
attr.st_mtime_ns = now_ns attr.st_mtime_ns = mtime_ns
attr.st_ctime_ns = now_ns attr.st_ctime_ns = mtime_ns
attr.generation = 0 attr.generation = 0
attr.entry_timeout = 5.0 attr.entry_timeout = 5.0
attr.attr_timeout = 5.0 attr.attr_timeout = 5.0
# Determine permissions based on readonly property
if is_tree: if is_tree:
attr.st_mode = stat.S_IFDIR | 0o755 mode = 0o755 if not note.readonly else 0o555
attr.st_mode = stat.S_IFDIR | mode
attr.st_size = 0 attr.st_size = 0
attr.st_blksize = 512 attr.st_blksize = 512
attr.st_blocks = 0 attr.st_blocks = 0
else: else:
attr.st_mode = stat.S_IFREG | 0o644 mode = 0o644 if not note.readonly else 0o444
attr.st_mode = stat.S_IFREG | mode
attr.st_size = size attr.st_size = size
attr.st_blksize = 512 attr.st_blksize = 512
attr.st_blocks = (size + 511) // 512 attr.st_blocks = (size + 511) // 512

View file

@ -1,6 +1,7 @@
from typing import Protocol, runtime_checkable, Optional, Dict, List, Self, NamedTuple, Iterable, TypedDict from typing import Protocol, runtime_checkable, Optional, Dict, List, Self, NamedTuple, Iterable, TypedDict
from uuid import UUID from uuid import UUID
from pathlib import PurePosixPath from pathlib import PurePosixPath
import datetime as dt
type ObjectId = int | str | UUID type ObjectId = int | str | UUID
@ -26,11 +27,27 @@ class Note(Protocol):
Protocol for a Note item. Protocol for a Note item.
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) -> ObjectId: def object_id(self) -> ObjectId:
"""The unique identifier for this note.""" """The unique identifier for this note."""
... ...
@property
def mime(self) -> str:
"""The MIME type of the note's content."""
...
@property
def readonly(self) -> bool:
"""Whether the note is read-only."""
...
@property
def mtime(self) -> dt.datetime:
"""The last modification time of the note."""
...
def get_raw_metadata(self, key: str) -> Optional[bytes]: def get_raw_metadata(self, key: str) -> Optional[bytes]:
"""Retrieve metadata value for the given key.""" """Retrieve metadata value for the given key."""
... ...
@ -84,10 +101,12 @@ class Tree(Protocol):
"""Return all entries as {name: object_id}.""" """Return all entries as {name: object_id}."""
... ...
class BlobNote(Note, Blob): @runtime_checkable
class BlobNote(Note, Blob, Protocol):
"""Blob Note""" """Blob Note"""
class TreeNote(Note, Tree): @runtime_checkable
class TreeNote(Note, Tree, Protocol):
"""Tree Note""" """Tree Note"""

View file

@ -8,6 +8,7 @@ tree serialization respectively.
from typing import Optional, Self from typing import Optional, Self
from pathlib import Path from pathlib import Path
import datetime as dt
from .db import Sqlite3Trove, NOTE_ROOT_ID from .db import Sqlite3Trove, NOTE_ROOT_ID
from .tree import Tree as TreeData from .tree import Tree as TreeData
@ -30,6 +31,20 @@ class NoteImpl(Note):
def object_id(self) -> int: def object_id(self) -> int:
return self._object_id return self._object_id
@property
def readonly(self) -> bool:
return False
@property
def mtime(self) -> dt.datetime:
"""Return modification time as Unix timestamp, or None if not set."""
return self._db.get_mtime(self._object_id)
@property
def mime(self) -> str:
"""Return MIME type, defaulting to generic binary stream."""
return "application/octet-stream"
def get_raw_metadata(self, key: str) -> Optional[bytes]: def get_raw_metadata(self, key: str) -> Optional[bytes]:
return self._db.read_metadata(self._object_id, key) return self._db.read_metadata(self._object_id, key)