lib.util.copy(): Support FileContext source, file types, and --parents #144
2 changed files with 68 additions and 3 deletions
lib.util.copy(): Add parents flag parameter
copy() lands a source below the destination root, taking at most the source's basename, so there is no way to land it at its source path, as cp --parents does. Add a parents flag that requests just that. 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
0ca539dc0f
|
|
@ -176,6 +176,7 @@ async def copy(
|
||||||
mode: int | None = None,
|
mode: int | None = None,
|
||||||
throw: bool = True,
|
throw: bool = True,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
|
parents: bool = False,
|
||||||
) -> Exception | str | list[str]:
|
) -> Exception | str | list[str]:
|
||||||
|
|
||||||
# Destination address / addresses
|
# Destination address / addresses
|
||||||
|
|
@ -183,7 +184,7 @@ async def copy(
|
||||||
if not isinstance(src, (str, FileContext)):
|
if not isinstance(src, (str, FileContext)):
|
||||||
ret = []
|
ret = []
|
||||||
for uri in src: # 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, parents)
|
||||||
if isinstance(rr, Exception):
|
if isinstance(rr, Exception):
|
||||||
return rr
|
return rr
|
||||||
if isinstance(rr, list):
|
if isinstance(rr, list):
|
||||||
|
|
@ -225,10 +226,32 @@ async def copy(
|
||||||
|
|
||||||
async with CopyContext(src, dst) as ctx:
|
async with CopyContext(src, dst) as ctx:
|
||||||
|
|
||||||
|
src_uri = src.uri if isinstance(src, FileContext) else Uri(src)
|
||||||
ret = '' if ctx.dst.chroot else ctx.dst.root
|
ret = '' if ctx.dst.chroot else ctx.dst.root
|
||||||
if ret:
|
if parents:
|
||||||
|
# -- cp --parents: the source lands at its source path
|
||||||
|
# below the destination root, each parent level
|
||||||
|
# typed by the source-side node it mirrors
|
||||||
|
parts = [p for p in src_uri.path.split('/') if p]
|
||||||
|
if parts:
|
||||||
|
src_path = '/'.join(parts)
|
||||||
|
base = ret
|
||||||
|
ret = f'{base}/{src_path}' if base else src_path
|
||||||
|
s_cur = ''
|
||||||
|
for part in parts[:-1]:
|
||||||
|
s_cur = f'{s_cur}/{part}' if s_cur else part
|
||||||
|
d_cur = f'{base}/{s_cur}' if base else s_cur
|
||||||
|
if await ctx.dst.is_dir(d_cur):
|
||||||
|
continue
|
||||||
|
level_ctx = FileContext.create(
|
||||||
|
f'{ctx.src.uri.scheme_plus_authority}/{s_cur}',
|
||||||
|
chroot = True,
|
||||||
|
)
|
||||||
|
async with level_ctx:
|
||||||
|
ftype = (await level_ctx.stat('')).ftype
|
||||||
|
await ctx.dst.mkdir(d_cur, ftype = ftype)
|
||||||
|
elif ret:
|
||||||
# Land the source's basename within existing destination directories
|
# Land the source's basename within existing destination directories
|
||||||
src_uri = src.uri if isinstance(src, FileContext) else Uri(src)
|
|
||||||
basename = os.path.basename(src_uri.path.rstrip('/'))
|
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
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,48 @@ async def run_tests() -> None:
|
||||||
assert isinstance(ret, list), ret
|
assert isinstance(ret, list), ret
|
||||||
assert os.path.join(tmp, 'dst/slash/a/f1') in ret, ret
|
assert os.path.join(tmp, 'dst/slash/a/f1') in ret, ret
|
||||||
|
|
||||||
|
# -- parents lands the object at its source path below the
|
||||||
|
# destination root, like cp --parents
|
||||||
|
dst_parents = os.path.join(tmp, 'dst/parents')
|
||||||
|
os.makedirs(dst_parents)
|
||||||
|
src_single = os.path.join(tmp, 'src/single')
|
||||||
|
ret = await copy(uri(src_single), uri(dst_parents), parents = True)
|
||||||
|
assert ret == dst_parents + src_single, ret
|
||||||
|
with open(dst_parents + src_single) as f:
|
||||||
|
assert f.read() == 'solo\n'
|
||||||
|
|
||||||
|
# -- parents on a tree lands the tree at its source path
|
||||||
|
dst_ptree = os.path.join(tmp, 'dst/ptree')
|
||||||
|
os.makedirs(dst_ptree)
|
||||||
|
src_tree = os.path.join(tmp, 'src/a')
|
||||||
|
ret = await copy(
|
||||||
|
uri(src_tree), uri(dst_ptree), recursive = True, parents = True
|
||||||
|
)
|
||||||
|
assert isinstance(ret, list), ret
|
||||||
|
expected = dst_ptree + src_tree
|
||||||
|
assert sorted(ret) == sorted([
|
||||||
|
os.path.join(expected, 'f1'),
|
||||||
|
os.path.join(expected, 'sub/f2'),
|
||||||
|
os.path.join(expected, 'sub/subsub/f3'),
|
||||||
|
]), ret
|
||||||
|
assert os.path.isdir(os.path.join(expected, 'empty')), ret
|
||||||
|
|
||||||
|
# -- a chrooted context source lands at its URI path
|
||||||
|
dst_pchroot = os.path.join(tmp, 'dst/pchroot')
|
||||||
|
os.makedirs(dst_pchroot)
|
||||||
|
src_fc = Local(uri(src_tree), chroot = True)
|
||||||
|
ret = await copy(src_fc, uri(dst_pchroot), recursive = True, parents = True)
|
||||||
|
assert isinstance(ret, list), ret
|
||||||
|
assert os.path.isfile(os.path.join(dst_pchroot + src_tree, 'f1')), ret
|
||||||
|
|
||||||
|
# -- re-copying with parents replaces content, not layout
|
||||||
|
with open(dst_parents + src_single, 'w') as f:
|
||||||
|
f.write('replaced\n')
|
||||||
|
ret = await copy(uri(src_single), uri(dst_parents), parents = True)
|
||||||
|
assert ret == dst_parents + src_single, ret
|
||||||
|
with open(dst_parents + src_single) as f:
|
||||||
|
assert f.read() == 'solo\n'
|
||||||
|
|
||||||
# -- A missing source fails loudly
|
# -- A missing source fails loudly
|
||||||
try:
|
try:
|
||||||
await copy(
|
await copy(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue