diff --git a/trovedb/db.py b/trovedb/db.py index e811028..72ee4c0 100644 --- a/trovedb/db.py +++ b/trovedb/db.py @@ -71,7 +71,7 @@ class Sqlite3Trove: con.commit() obj = cls(con) if initialize: - obj.write_blob(b"", NODE_ROOT_ID) + obj.write_blob(b"", NOTE_ROOT_ID) return obj def close(self): @@ -103,6 +103,15 @@ class Sqlite3Trove: return None 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: """Return raw metadata value for (uuid, key), or None if not found.""" row = self._con.execute( diff --git a/trovedb/fs.py b/trovedb/fs.py index ff6eece..3f36792 100644 --- a/trovedb/fs.py +++ b/trovedb/fs.py @@ -1,6 +1,7 @@ import os import sqlite3 import tempfile +import datetime as dt from pathlib import Path from typing import Optional, Dict, List, Self, Iterable 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") 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 def _path(self) -> Path: if self._fs_path is not None: @@ -60,6 +79,11 @@ class FSBlobNote(FSNote, BlobNote): pass 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): if not isinstance(note, FSBlobNote): raise BadNoteType("Only blob notes can be linked") diff --git a/trovedb/fuse/server.py b/trovedb/fuse/server.py index 2441314..f933972 100644 --- a/trovedb/fuse/server.py +++ b/trovedb/fuse/server.py @@ -194,20 +194,26 @@ class TroveFuseOps(pyfuse3.Operations): attr.st_nlink = 1 attr.st_uid = os.getuid() attr.st_gid = os.getgid() + + mtime_ns = int(note.mtime.timestamp() * 1e9) now_ns = int(time.time() * 1e9) attr.st_atime_ns = now_ns - attr.st_mtime_ns = now_ns - attr.st_ctime_ns = now_ns + attr.st_mtime_ns = mtime_ns + attr.st_ctime_ns = mtime_ns attr.generation = 0 attr.entry_timeout = 5.0 attr.attr_timeout = 5.0 + + # Determine permissions based on readonly property 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_blksize = 512 attr.st_blocks = 0 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_blksize = 512 attr.st_blocks = (size + 511) // 512 diff --git a/trovedb/trove.py b/trovedb/trove.py index c9d0be9..924331f 100644 --- a/trovedb/trove.py +++ b/trovedb/trove.py @@ -1,6 +1,7 @@ from typing import Protocol, runtime_checkable, Optional, Dict, List, Self, NamedTuple, Iterable, TypedDict from uuid import UUID from pathlib import PurePosixPath +import datetime as dt type ObjectId = int | str | UUID @@ -26,11 +27,27 @@ class Note(Protocol): Protocol for a Note item. Represents access to an individual note's content and metadata. """ + @property def object_id(self) -> ObjectId: """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]: """Retrieve metadata value for the given key.""" ... @@ -84,10 +101,12 @@ class Tree(Protocol): """Return all entries as {name: object_id}.""" ... -class BlobNote(Note, Blob): +@runtime_checkable +class BlobNote(Note, Blob, Protocol): """Blob Note""" -class TreeNote(Note, Tree): +@runtime_checkable +class TreeNote(Note, Tree, Protocol): """Tree Note""" diff --git a/trovedb/trovedb.py b/trovedb/trovedb.py index 1cf2f2e..7141be4 100644 --- a/trovedb/trovedb.py +++ b/trovedb/trovedb.py @@ -8,6 +8,7 @@ tree serialization respectively. from typing import Optional, Self from pathlib import Path +import datetime as dt from .db import Sqlite3Trove, NOTE_ROOT_ID from .tree import Tree as TreeData @@ -30,6 +31,20 @@ class NoteImpl(Note): def object_id(self) -> int: 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]: return self._db.read_metadata(self._object_id, key)