Files
HCFS/hcfs-python/hcfs/core/filesystem.py
2025-07-30 09:34:16 +10:00

179 lines
6.6 KiB
Python

"""
HCFS Filesystem - FUSE-based virtual filesystem layer.
"""
import os
import stat
import errno
import time
from typing import Dict, Optional
from pathlib import Path
import pyfuse3
from pyfuse3 import FUSEError
from .context_db import ContextDatabase, Context
class HCFSFilesystem(pyfuse3.Operations):
"""
HCFS FUSE filesystem implementation.
Maps directory navigation to context scope and provides
virtual files for context access.
"""
def __init__(self, context_db: ContextDatabase, mount_point: str):
super().__init__()
self.context_db = context_db
self.mount_point = mount_point
self._inode_counter = 1
self._inode_to_path: Dict[int, str] = {1: "/"} # Root inode
self._path_to_inode: Dict[str, int] = {"/": 1}
# Virtual files
self.CONTEXT_FILE = ".context"
self.CONTEXT_LIST_FILE = ".context_list"
self.CONTEXT_PUSH_FILE = ".context_push"
def _get_inode(self, path: str) -> int:
"""Get or create inode for path."""
if path in self._path_to_inode:
return self._path_to_inode[path]
self._inode_counter += 1
inode = self._inode_counter
self._inode_to_path[inode] = path
self._path_to_inode[path] = inode
return inode
def _get_path(self, inode: int) -> str:
"""Get path for inode."""
return self._inode_to_path.get(inode, "/")
def _is_virtual_file(self, path: str) -> bool:
"""Check if path is a virtual context file."""
basename = os.path.basename(path)
return basename in [self.CONTEXT_FILE, self.CONTEXT_LIST_FILE, self.CONTEXT_PUSH_FILE]
async def getattr(self, inode: int, ctx=None) -> pyfuse3.EntryAttributes:
"""Get file attributes."""
path = self._get_path(inode)
entry = pyfuse3.EntryAttributes()
entry.st_ino = inode
entry.st_uid = os.getuid()
entry.st_gid = os.getgid()
entry.st_atime_ns = int(time.time() * 1e9)
entry.st_mtime_ns = int(time.time() * 1e9)
entry.st_ctime_ns = int(time.time() * 1e9)
if self._is_virtual_file(path):
# Virtual files are readable text files
entry.st_mode = stat.S_IFREG | 0o644
entry.st_size = 1024 # Placeholder size
else:
# Directories
entry.st_mode = stat.S_IFDIR | 0o755
entry.st_size = 0
return entry
async def lookup(self, parent_inode: int, name: bytes, ctx=None) -> pyfuse3.EntryAttributes:
"""Look up a directory entry."""
parent_path = self._get_path(parent_inode)
child_path = os.path.join(parent_path, name.decode('utf-8'))
# Normalize path
if child_path.startswith("//"):
child_path = child_path[1:]
child_inode = self._get_inode(child_path)
return await self.getattr(child_inode, ctx)
async def opendir(self, inode: int, ctx=None) -> int:
"""Open directory."""
return inode
async def readdir(self, inode: int, start_id: int, token) -> None:
"""Read directory contents."""
path = self._get_path(inode)
# Always show virtual context files in every directory
entries = [
(self.CONTEXT_FILE, await self.getattr(self._get_inode(os.path.join(path, self.CONTEXT_FILE)))),
(self.CONTEXT_LIST_FILE, await self.getattr(self._get_inode(os.path.join(path, self.CONTEXT_LIST_FILE)))),
(self.CONTEXT_PUSH_FILE, await self.getattr(self._get_inode(os.path.join(path, self.CONTEXT_PUSH_FILE)))),
]
# Add subdirectories (you might want to make this dynamic based on context paths)
# For now, allowing any directory to be created by navigation
for i, (name, attr) in enumerate(entries):
if i >= start_id:
if not pyfuse3.readdir_reply(token, name.encode('utf-8'), attr, i + 1):
break
async def open(self, inode: int, flags: int, ctx=None) -> int:
"""Open file."""
path = self._get_path(inode)
if not self._is_virtual_file(path):
raise FUSEError(errno.EISDIR)
return inode
async def read(self, fh: int, offset: int, size: int) -> bytes:
"""Read from virtual files."""
path = self._get_path(fh)
basename = os.path.basename(path)
dir_path = os.path.dirname(path)
if basename == self.CONTEXT_FILE:
# Return aggregated context for current directory
contexts = self.context_db.get_context_by_path(dir_path, depth=1)
content = "\\n".join(f"[{ctx.path}] {ctx.content}" for ctx in contexts)
elif basename == self.CONTEXT_LIST_FILE:
# List contexts at current path
contexts = self.context_db.list_contexts_at_path(dir_path)
content = "\\n".join(f"ID: {ctx.id}, Path: {ctx.path}, Author: {ctx.author}, Created: {ctx.created_at}"
for ctx in contexts)
elif basename == self.CONTEXT_PUSH_FILE:
# Instructions for pushing context
content = f"Write to this file to push context to path: {dir_path}\\nFormat: <content>"
else:
content = "Unknown virtual file"
content_bytes = content.encode('utf-8')
return content_bytes[offset:offset + size]
async def write(self, fh: int, offset: int, data: bytes) -> int:
"""Write to virtual files (context_push only)."""
path = self._get_path(fh)
basename = os.path.basename(path)
dir_path = os.path.dirname(path)
if basename == self.CONTEXT_PUSH_FILE:
# Push new context to current directory
content = data.decode('utf-8').strip()
context = Context(
id=None,
path=dir_path,
content=content,
author="fuse_user"
)
self.context_db.store_context(context)
return len(data)
else:
raise FUSEError(errno.EACCES)
async def mkdir(self, parent_inode: int, name: bytes, mode: int, ctx=None) -> pyfuse3.EntryAttributes:
"""Create directory (virtual - just for navigation)."""
parent_path = self._get_path(parent_inode)
new_path = os.path.join(parent_path, name.decode('utf-8'))
if new_path.startswith("//"):
new_path = new_path[1:]
new_inode = self._get_inode(new_path)
return await self.getattr(new_inode, ctx)