179 lines
6.6 KiB
Python
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) |