lib.util.copy(): Accept a FileContext as source

Using a FileContext, notably with its chroot flag set, is useful as a
tree source in copy(), so allow it.

Signed-off-by: Jan Lindemann <jan@janware.com>
Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v1.1.0
This commit is contained in:
Jan Lindemann 2026-10-04 13:55:39 +02:00
commit 482daa17e7
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61
2 changed files with 32 additions and 7 deletions

View file

@ -8,6 +8,7 @@ from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Iterable, TypeVar, cast from typing import TYPE_CHECKING, Any, Iterable, TypeVar, cast
from .base import Input, InputMode, Result from .base import Input, InputMode, Result
from .FileContext import FileContext
from .log import DEBUG, ERR, log from .log import DEBUG, ERR, log
from .Uri import Uri from .Uri import Uri
@ -15,7 +16,6 @@ if TYPE_CHECKING:
from argparse import Namespace from argparse import Namespace
from .ExecContext import ExecContext from .ExecContext import ExecContext
from .FileContext import FileContext
from .ProcFilter import ProcFilter, ProcPipeline from .ProcFilter import ProcFilter, ProcPipeline
T = TypeVar('T') T = TypeVar('T')
@ -169,7 +169,7 @@ async def get(
return await run_pipeline(await ctx.get(uri.path, *args, **kwargs), content_filter) return await run_pipeline(await ctx.get(uri.path, *args, **kwargs), content_filter)
async def copy( async def copy(
src_uri: str | Iterable[str], src: str | FileContext | Iterable[str],
dst: str | FileContext, dst: str | FileContext,
owner: str | None = None, owner: str | None = None,
group: str | None = None, group: str | None = None,
@ -180,9 +180,9 @@ async def copy(
# Destination address / addresses # Destination address / addresses
ret: str | list[str] ret: str | list[str]
if not isinstance(src_uri, str): if not isinstance(src, (str, FileContext)):
ret = [] ret = []
for uri in src_uri: # TODO: Group identical netlocs into one CopyContext for uri in src: # TODO: Group identical netlocs into one CopyContext
rr = await copy(uri, dst, owner, group, mode, throw, recursive) rr = await copy(uri, dst, owner, group, mode, throw, recursive)
if isinstance(rr, Exception): if isinstance(rr, Exception):
return rr return rr
@ -223,12 +223,13 @@ async def copy(
from .CopyContext import CopyContext from .CopyContext import CopyContext
async with CopyContext(src_uri, dst) as ctx: async with CopyContext(src, dst) as ctx:
ret = '' if ctx.dst.chroot else ctx.dst.root ret = '' if ctx.dst.chroot else ctx.dst.root
if ret: if ret:
# Land the source's basename within existing destination directories # Land the source's basename within existing destination directories
basename = os.path.basename(Uri(src_uri).path.rstrip('/')) src_uri = src.uri if isinstance(src, FileContext) else Uri(src)
basename = os.path.basename(src_uri.path.rstrip('/'))
if basename and await ctx.dst.is_dir(ret): if basename and await ctx.dst.is_dir(ret):
ret += '/' + basename ret += '/' + basename
src_root = '' if ctx.src.chroot else ctx.src.root src_root = '' if ctx.src.chroot else ctx.src.root
@ -251,7 +252,7 @@ async def copy(
except Exception as e: except Exception as e:
if throw: if throw:
raise raise
log(ERR, f'Failed to copy {src_uri} -> {dst} ({str(e)})') log(ERR, f'Failed to copy {src} -> {dst} ({str(e)})')
return e return e
async def get_username( # export async def get_username( # export

View file

@ -3,6 +3,7 @@ import os
import stat import stat
import tempfile import tempfile
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.util import copy from jw.pkg.lib.util import copy
def uri(path: str) -> str: def uri(path: str) -> str:
@ -99,6 +100,29 @@ async def run_tests() -> None:
st = os.stat(os.path.join(dst_mode, 'sub/subsub/f3')) st = os.stat(os.path.join(dst_mode, 'sub/subsub/f3'))
assert stat.S_IMODE(st.st_mode) == 0o640, st assert stat.S_IMODE(st.st_mode) == 0o640, st
# -- A chrooted context source: the empty address is the
# context's root, and landing into an existing directory
# takes the root's basename
os.makedirs(os.path.join(tmp, 'dst/chroot'))
src_fc = Local(uri(os.path.join(tmp, 'src/a')), chroot = True)
ret = await copy(src_fc, uri(os.path.join(tmp, 'dst/chroot')), recursive = True)
assert os.path.isfile(os.path.join(tmp, 'dst/chroot/a/f1')), ret
assert os.path.isfile(os.path.join(tmp, 'dst/chroot/a/sub/f2')), ret
# -- A trailing slash does not change the source's name
os.makedirs(os.path.join(tmp, 'dst/slash'))
ret = await copy(
uri(os.path.join(tmp, 'src/a/')),
uri(os.path.join(tmp, 'dst/slash')),
recursive = True,
)
assert isinstance(ret, list), ret
assert os.path.join(tmp, 'dst/slash/a/f1') in ret, ret
src_fc = Local(uri(os.path.join(tmp, 'src/a/')), chroot = True)
ret = await copy(src_fc, uri(os.path.join(tmp, 'dst/slash')), recursive = True)
assert isinstance(ret, list), ret
assert os.path.join(tmp, 'dst/slash/a/f1') in ret, ret
# -- A missing source fails loudly # -- A missing source fails loudly
try: try:
await copy( await copy(