| """ |
| Read interface over a Vault, plus an optional WinFsp mount. |
| |
| `VaultFS` is a dependency-free read model: list files, stat, and read byte ranges |
| that are reconstructed (and RS-healed) on demand from the Vault. This is what the |
| CLI `export` uses and what the mount serves. |
| |
| `mount()` exposes the Vault as a real drive letter via WinFsp (Windows FUSE). It |
| is guarded: if `winfspy` / WinFsp is not installed, it raises with instructions |
| instead of failing obscurely. The dependency-free path (VaultFS + CLI export) |
| works everywhere; the live drive lights up once WinFsp is present. |
| """ |
| from __future__ import annotations |
| from collections import OrderedDict |
| from .vault import Vault |
|
|
|
|
| class VaultFS: |
| def __init__(self, vault: Vault, cache_files: int = 8): |
| self.vault = vault |
| self._cache: OrderedDict[str, bytes] = OrderedDict() |
| self._cache_n = cache_files |
| self._index = {rel: sz for rel, sz in vault.list_files()} |
|
|
| def listdir(self): |
| return sorted(self._index.items()) |
|
|
| def size(self, relpath: str) -> int: |
| return self._index[relpath] |
|
|
| def _data(self, relpath: str) -> bytes: |
| if relpath in self._cache: |
| self._cache.move_to_end(relpath) |
| return self._cache[relpath] |
| data = self.vault.read_file(relpath) |
| self._cache[relpath] = data |
| if len(self._cache) > self._cache_n: |
| self._cache.popitem(last=False) |
| return data |
|
|
| def read(self, relpath: str, offset: int = 0, length: int | None = None) -> bytes: |
| data = self._data(relpath) |
| if length is None: |
| return data[offset:] |
| return data[offset:offset + length] |
|
|
|
|
| def winfsp_available() -> bool: |
| try: |
| import winfspy |
| return True |
| except Exception: |
| return False |
|
|
|
|
| def mount(vault: Vault, mountpoint: str, label: str = "NeuralVault"): |
| """Mount the vault as a read-only drive via WinFsp. Requires WinFsp + winfspy. |
| Experimental: the dependency-free `export` path is the tested one.""" |
| if not winfsp_available(): |
| raise RuntimeError( |
| "WinFsp / winfspy not found. Install WinFsp (https://winfsp.dev) and " |
| "`pip install winfspy`, or use the dependency-free CLI: " |
| "`python cli.py export <vault> <folder>`." |
| ) |
| |
| from winfspy import (FileSystem, BaseFileSystemOperations, NTStatusObjectNameNotFound, |
| FILE_ATTRIBUTE) |
| import time |
|
|
| fs_view = VaultFS(vault) |
| now = int(time.time() * 1e7) + 116444736000000000 |
|
|
| class Ops(BaseFileSystemOperations): |
| def _info(self, is_dir, size=0): |
| return { |
| "file_attributes": FILE_ATTRIBUTE.FILE_ATTRIBUTE_DIRECTORY if is_dir |
| else FILE_ATTRIBUTE.FILE_ATTRIBUTE_NORMAL, |
| "allocation_size": size, "file_size": size, |
| "creation_time": now, "last_access_time": now, |
| "last_write_time": now, "change_time": now, "index_number": 0, |
| } |
|
|
| def _norm(self, p): |
| return p.replace("\\", "/").lstrip("/") |
|
|
| def get_volume_info(self): |
| return {"total_size": 0, "free_size": 0, "volume_label": label} |
|
|
| def get_security_by_name(self, file_name): |
| rel = self._norm(file_name) |
| is_dir = rel == "" or any(f.startswith(rel + "/") for f in fs_view._index) |
| if not is_dir and rel not in fs_view._index: |
| raise NTStatusObjectNameNotFound() |
| return (self._info(is_dir, 0 if is_dir else fs_view.size(rel))["file_attributes"], None, 0) |
|
|
| def open(self, file_name, create_options, granted_access): |
| rel = self._norm(file_name) |
| is_dir = rel == "" or any(f.startswith(rel + "/") for f in fs_view._index) |
| if not is_dir and rel not in fs_view._index: |
| raise NTStatusObjectNameNotFound() |
| return {"rel": rel, "is_dir": is_dir} |
|
|
| def get_file_info(self, fh): |
| return self._info(fh["is_dir"], 0 if fh["is_dir"] else fs_view.size(fh["rel"])) |
|
|
| def read(self, fh, offset, length): |
| return fs_view.read(fh["rel"], offset, length) |
|
|
| def read_directory(self, fh, marker): |
| prefix = "" if fh["rel"] == "" else fh["rel"] + "/" |
| names = set() |
| for f in fs_view._index: |
| if f.startswith(prefix): |
| rest = f[len(prefix):] |
| names.add(rest.split("/")[0]) |
| out = [] |
| for nm in sorted(names): |
| full = prefix + nm |
| is_dir = full not in fs_view._index |
| out.append({"file_name": nm, |
| "file_info": self._info(is_dir, 0 if is_dir else fs_view.size(full))}) |
| return out |
|
|
| def close(self, fh): |
| pass |
|
|
| fs = FileSystem(mountpoint, Ops(), volume_label=label) |
| fs.start() |
| return fs |
|
|