lib.util.copy(): Support FileContext source, file types, and --parents #144
2 changed files with 32 additions and 7 deletions
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
commit
482daa17e7
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue