Add more meta information to trove entries
This commit is contained in:
parent
4ada169bbe
commit
bd4d571c95
5 changed files with 80 additions and 7 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue