lib.util.copy(): Support FileContext source, file types, and --parents #144

Merged
Jan Lindemann merged 8 commits from jan/feature/20261017-lib-util-copy-support-filecontext-source-file-types-and-parents into master 2026-10-17 11:57:05 +02:00 AGit
Showing only changes of commit a328b01e9a - Show all commits

lib.util.copy(): Remove __mkdir_p()

Refactor copy(), making the recursive logic more coherent: Every node the
walk creates sits below a node that already exists, so a plain per-node
mkdir() suffices and the chain- creating __mkdir_p() is dropped: the
landing root's parent must exist now, as in cp -r.

Signed-off-by: Jan Lindemann <jan@janware.com>
Jan Lindemann 2026-10-04 10:23:48 +02:00
Signed by: Jan Lindemann
GPG key ID: 3750640C9E25DD61

View file

@ -177,8 +177,11 @@ async def copy(
throw: bool = True, throw: bool = True,
recursive: bool = False, recursive: bool = False,
) -> Exception | str | list[str]: ) -> Exception | str | list[str]:
# Destination address / addresses
ret: str | list[str]
if not isinstance(src_uri, str): if not isinstance(src_uri, str):
ret: list[str] = [] ret = []
for uri in src_uri: # TODO: Group identical netlocs into one CopyContext for uri in src_uri: # 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):
@ -190,72 +193,64 @@ async def copy(
else: else:
raise Exception(f'copy() returned unexpected type {type(rr)}') raise Exception(f'copy() returned unexpected type {type(rr)}')
return ret return ret
from .CopyContext import CopyContext
async with CopyContext(src_uri, dst) as ctx: async def __copy_tree(src_path: str, dst_path: str) -> list[str]:
ret: list[str] = []
async def __mkdir_p(path: str) -> None: s_st = await ctx.src.stat(src_path)
# -- Create each level of the destination path that does not if dst_path and not await ctx.dst.is_dir(dst_path):
# exist yet; put() does not create parent directories await ctx.dst.mkdir(dst_path)
cur = '' for dirent in await ctx.src.readdir(src_path):
for part in path.strip('/').split('/'): s_path = f'{src_path}/{dirent}' if src_path else dirent
cur = f'{cur}/{part}' d_path = f'{dst_path}/{dirent}' if dst_path else dirent
if not await ctx.dst.is_dir(cur): s_st = await ctx.src.stat(s_path)
await ctx.dst.mkdir(cur) if s_st.is_dir:
ret.extend(await __copy_tree(s_path, d_path))
async def __copy_tree(src_path: str, dst_path: str) -> list[str]: continue
ret: list[str] = [] result = await ctx.src.get(s_path, throw = True)
for entry in await ctx.src.readdir(src_path):
s_path = f'{src_path}/{entry}'
d_path = f'{dst_path}/{entry}'
if await ctx.src.is_dir(s_path):
await __mkdir_p(d_path)
ret.extend(await __copy_tree(s_path, d_path))
else:
result = await ctx.src.get(s_path, throw = True)
parent = d_path.rpartition('/')[0]
if parent:
await __mkdir_p(parent)
await ctx.dst.put(
path = d_path,
content = result.stdout,
owner = owner,
group = group,
mode = mode,
throw = True,
)
ret.append(d_path)
return ret
try:
if recursive and await ctx.src.is_dir(ctx.src.root):
# -- A directory source is mirrored below the destination
# root: into it, if the root is an existing directory,
# as it, if it does not exist yet
dst_root = ctx.dst.root
if await ctx.dst.is_dir(dst_root):
dst_root += '/' + os.path.basename(src_uri)
await __mkdir_p(dst_root)
return await __copy_tree(ctx.src.root, dst_root)
result = await ctx.src.get(ctx.src.root, throw = True)
dst_path = ctx.dst.root
if await ctx.dst.is_dir(ctx.dst.root):
dst_path += '/' + os.path.basename(src_uri)
await ctx.dst.put( await ctx.dst.put(
path = dst_path, path = d_path,
content = result.stdout, content = result.stdout,
owner = owner, owner = owner,
group = group, group = group,
mode = mode, mode = mode,
throw = True, throw = True,
) )
return dst_path ret.append(d_path)
except Exception as e: return ret
if throw:
raise try:
log(ERR, f'Failed to copy {src_uri} -> {dst} ({str(e)})')
return e from .CopyContext import CopyContext
assert False, 'Unreachable code'
async with CopyContext(src_uri, dst) as ctx:
ret = '' if ctx.dst.chroot else ctx.dst.root
if ret:
# Land the source's basename within existing destination directories
basename = os.path.basename(Uri(src_uri).path.rstrip('/'))
if basename and await ctx.dst.is_dir(ret):
ret += '/' + basename
src_root = '' if ctx.src.chroot else ctx.src.root
if recursive and await ctx.src.is_dir(src_root):
return await __copy_tree(src_root, ret)
result = await ctx.src.get(src_root, throw = True)
await ctx.dst.put(
path = ret,
content = result.stdout,
owner = owner,
group = group,
mode = mode,
throw = True,
)
return ret
except Exception as e:
if throw:
raise
log(ERR, f'Failed to copy {src_uri} -> {dst} ({str(e)})')
return e
async def get_username( # export async def get_username( # export
args: Namespace | None = None, args: Namespace | None = None,