Add a file context that sits in front of another one and delegates every primitive call to it. The wrapper adopts the identity of the wrapped context: uri, root, chroot flag, and the chroot path composition are the wrapped context's, so a path composed by the wrapper composes identically in the wrapped context. Every primitive is forwarded to the wrapped context's primitives, which receive the already-composed path; open() and close() are forwarded to the wrapped context's public methods, so the open count is kept in one place. The point is to give derived contexts a stable base: a caching context, or any other filter, overrides the primitives it cares about and inherits the rest, instead of re-deriving the chroot bookkeeping of every method. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.1.0 Signed-off-by: Jan Lindemann <jan@janware.com>
161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from typing import TYPE_CHECKING, Iterable, override
|
|
|
|
if TYPE_CHECKING:
|
|
from jw.pkg.lib.file_types import FileType
|
|
|
|
from jw.pkg.lib.base import StatResult
|
|
from jw.pkg.lib.FcWrapper import FcWrapper
|
|
from jw.pkg.lib.file_types import FT_DIR, FT_REG
|
|
from jw.pkg.lib.FileContext import FileContext
|
|
from jw.pkg.lib.Result import Result
|
|
|
|
class FakeCtx(FileContext):
|
|
"""An in-memory FileContext that counts its primitive calls"""
|
|
|
|
def __init__(self, uri: str, chroot: bool = False) -> None:
|
|
super().__init__(uri, chroot = chroot)
|
|
self.files: dict[str, bytes] = {}
|
|
self.dirs: set[str] = set()
|
|
self.counts: dict[str, int] = {}
|
|
self.opens = 0
|
|
|
|
def count(self, op: str) -> None:
|
|
self.counts[op] = self.counts.get(op, 0) + 1
|
|
|
|
def _stat_of(self, path: str) -> StatResult:
|
|
if path in self.dirs:
|
|
return StatResult(0o40755, '0', '0', 40, 0.0, 0.0, 0.0)
|
|
if path in self.files:
|
|
return StatResult(0o100644, '0', '0', len(self.files[path]), 0.0, 0.0, 0.0)
|
|
raise FileNotFoundError(path)
|
|
|
|
@override
|
|
async def _open(self) -> None:
|
|
self.opens += 1
|
|
|
|
@override
|
|
async def _close(self) -> None:
|
|
pass
|
|
|
|
@override
|
|
async def _get(
|
|
self, path: str, wd: str | None, throw: bool, verbose: bool | None, title: str
|
|
) -> Result:
|
|
self.count('get')
|
|
if wd is not None:
|
|
path = wd + '/' + path
|
|
if path not in self.files:
|
|
raise FileNotFoundError(path)
|
|
return Result(stdout = self.files[path])
|
|
|
|
@override
|
|
async def _put(
|
|
self,
|
|
path: str,
|
|
content: bytes,
|
|
wd: str | None,
|
|
throw: bool,
|
|
verbose: bool | None,
|
|
title: str,
|
|
owner: str | None,
|
|
group: str | None,
|
|
mode: str | None,
|
|
atomic: bool,
|
|
ftype: FileType,
|
|
) -> Result:
|
|
self.count('put')
|
|
if wd is not None:
|
|
path = wd + '/' + path
|
|
self.files[path] = content
|
|
return Result(stdout = b'')
|
|
|
|
@override
|
|
async def _mkdir(self, path: str, mode: int, ftype: FileType) -> None:
|
|
self.count('mkdir')
|
|
self.dirs.add(path)
|
|
|
|
@override
|
|
async def _unlink(self, path: str) -> None:
|
|
self.count('unlink')
|
|
self.files.pop(path, None)
|
|
self.dirs.discard(path)
|
|
|
|
@override
|
|
async def _stat(self, path: str, follow_symlinks: bool) -> StatResult:
|
|
self.count('stat')
|
|
return self._stat_of(path)
|
|
|
|
@override
|
|
async def _file_exists(self, path: str) -> bool:
|
|
self.count('file_exists')
|
|
try:
|
|
self._stat_of(path)
|
|
return True
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
@override
|
|
async def _is_dir(self, path: str, follow_symlinks: bool) -> bool:
|
|
self.count('is_dir')
|
|
try:
|
|
st = self._stat_of(path)
|
|
return (st.mode & 0o170000) == 0o40000
|
|
except FileNotFoundError:
|
|
return False
|
|
|
|
@override
|
|
async def _readdir(self, path: str) -> Iterable[str]:
|
|
self.count('readdir')
|
|
prefix = path.rstrip('/') + '/'
|
|
return [
|
|
p[len(prefix):] for p in sorted(self.files.keys() | self.dirs)
|
|
if p.startswith(prefix)
|
|
]
|
|
|
|
async def main() -> None:
|
|
fake = FakeCtx('local:///tmp/fc-wrapper-fake', chroot = True)
|
|
fc = FcWrapper(fake)
|
|
|
|
# -- The wrapper adopts the identity of the wrapped context
|
|
assert fc.wrapped is fake
|
|
assert fc.uri is fake.uri
|
|
assert fc.root == fake.root
|
|
assert fc.chroot == fake.chroot
|
|
assert fc.username == fake.username
|
|
assert fc.interactive == fake.interactive
|
|
assert fc.verbose_default == fake.verbose_default
|
|
|
|
# -- Put and get are forwarded; paths are composed once
|
|
await fc.put('etc/a.conf', b'x=1\n', ftype = FT_REG)
|
|
assert fake.files == {'/tmp/fc-wrapper-fake/etc/a.conf': b'x=1\n'}
|
|
assert fake.counts['put'] == 1
|
|
|
|
ret = await fc.get('etc/a.conf')
|
|
assert ret.stdout == b'x=1\n'
|
|
assert fake.counts['get'] == 1
|
|
|
|
# -- stat, file_exists, is_dir, and readdir are forwarded
|
|
st = await fc.stat('etc/a.conf')
|
|
assert st.size == 4
|
|
assert await fc.file_exists('etc/a.conf') is True
|
|
assert await fc.file_exists('etc/missing.conf') is False
|
|
await fc.mkdir('etc/sub', 0o755, FT_DIR)
|
|
assert await fc.is_dir('etc/sub') is True
|
|
names = await fc.readdir('etc')
|
|
assert 'a.conf' in names and 'sub' in names
|
|
|
|
# -- Unlink is forwarded
|
|
await fc.unlink('etc/a.conf')
|
|
assert '/tmp/fc-wrapper-fake/etc/a.conf' not in fake.files
|
|
assert fake.counts['unlink'] == 1
|
|
|
|
# -- Open and close reach the wrapped context's public methods
|
|
await fc.open()
|
|
await fc.close()
|
|
assert fake.opens == 1
|
|
|
|
asyncio.run(main())
|