FileContext: Add readdir() and recursive directory copy #131

Merged
Jan Lindemann merged 2 commits from jan/feature/20260929-filecontext-add-readdir-and-recursive-directory-copy into master 2026-09-29 15:48:00 +02:00 AGit
Owner

FileContext could address single files but neither list a directory nor mirror a tree between contexts; this PR adds the missing pieces: readdir() enumerates directory entries portably across the context families, and util.copy() gains a recursive mode that uses it to mirror a directory source below the destination root.

lib.FileContext: Add readdir() method

FileContext offers get(), put(), unlink(), mkdir(), and stat(), but no way to list the entries of a directory, so consumers that need the names of the entries under a path in a context have no portable means to obtain them.

Add readdir(), returning Iterable[str], following the _readdir() -> readdir() pattern used by the other operations: the public method chroots the path via _chroot() and delegates to the _readdir() hook, which raises NotImplementedError by default.

Implement _readdir() in the context families:

  • Local._readdir(): os.listdir()
  • ExecContext._readdir(): runs ls -1A and splits the output into one entry per line; inherited by SSHClient and the other command-based contexts
  • Curl._readdir(): raises OSError with errno.ENOSYS

Add unit tests covering Local (listing, hidden entries, missing directory) and Curl (ENOSYS).

util.copy(): Add recursive directory copy

copy() copies a single file: it fetches the source root as one content and puts it at the destination, so addressing a directory source fails, and there is no way to mirror a tree between file contexts.

Add a recursive mode:

  • 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
  • entries are enumerated with readdir(), destination directories are created level by level, and every leaf is put with the requested owner, group, and mode
  • a file source is unaffected, and the destination parent must exist, as before

posix copy exposes the mode as -r/--recursive; the help test reference picks up the new option.

FileContext could address single files but neither list a directory nor mirror a tree between contexts; this PR adds the missing pieces: `readdir()` enumerates directory entries portably across the context families, and `util.copy()` gains a recursive mode that uses it to mirror a directory source below the destination root. #### lib.FileContext: Add readdir() method FileContext offers `get()`, `put()`, `unlink()`, `mkdir()`, and `stat()`, but no way to list the entries of a directory, so consumers that need the names of the entries under a path in a context have no portable means to obtain them. Add `readdir()`, returning `Iterable[str]`, following the `_readdir()` -> `readdir()` pattern used by the other operations: the public method chroots the path via `_chroot()` and delegates to the `_readdir()` hook, which raises `NotImplementedError` by default. Implement `_readdir()` in the context families: - `Local._readdir()`: `os.listdir()` - `ExecContext._readdir()`: runs `ls -1A` and splits the output into one entry per line; inherited by `SSHClient` and the other command-based contexts - `Curl._readdir()`: raises `OSError` with `errno.ENOSYS` Add unit tests covering `Local` (listing, hidden entries, missing directory) and `Curl` (`ENOSYS`). #### util.copy(): Add recursive directory copy `copy()` copies a single file: it fetches the source root as one content and puts it at the destination, so addressing a directory source fails, and there is no way to mirror a tree between file contexts. Add a recursive mode: - 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 - entries are enumerated with `readdir()`, destination directories are created level by level, and every leaf is put with the requested owner, group, and mode - a file source is unaffected, and the destination parent must exist, as before `posix copy` exposes the mode as `-r/--recursive`; the help test reference picks up the new option.
FileContext offers get(), put(), unlink(), mkdir(), and stat(), but no way
to list the entries of a directory, so consumers that need the names of the
entries under a path in a context have no portable means to obtain them.

Add readdir(), returning Iterable[str], following the _readdir() ->
readdir() pattern used by the other operations: the public method chroots
the path via _chroot() and delegates to the _readdir() hook, which raises
NotImplementedError by default.

Implement _readdir() in the context families:

- Local._readdir(): os.listdir()
- ExecContext._readdir(): runs ls -1A and splits the output into one entry
  per line; inherited by SSHClient and the other command-based contexts
- Curl._readdir(): raises OSError with errno.ENOSYS

Add unit tests covering Local (listing, hidden entries, missing directory)
and Curl (ENOSYS).

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.87.1
Signed-off-by: Jan Lindemann <jan@janware.com>
util.copy(): Add recursive directory copy
All checks were successful
CI / Packaging - Kali Linux (pull_request) Successful in 4m51s
CI / Packaging - OpenSUSE Tumbleweed (pull_request) Successful in 4m42s
CI / Packaging test (pull_request) Successful in 0s
CI / Packaging - Kali Linux (push) Successful in 4m53s
CI / Packaging - OpenSUSE Tumbleweed (push) Successful in 4m46s
CI / Packaging test (push) Successful in 0s
b697b7b754
copy() copies a single file: it fetches the source root as one content and
puts it at the destination, so addressing a directory source fails, and
there is no way to mirror a tree between file contexts.

Add a recursive mode:

- 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
- entries are enumerated with readdir(), destination directories are
  created level by level, and every leaf is put with the requested owner,
  group, and mode
- a file source is unaffected, and the destination parent must exist, as
  before

posix copy exposes the mode as -r/--recursive; the help test reference
picks up the new option.

Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.87.1
Signed-off-by: Jan Lindemann <jan@janware.com>
Jan Lindemann scheduled this pull request to auto merge when all checks succeed 2026-09-29 15:29:22 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
janware/jw-pkg!131
No description provided.