Skip to content

File properties and directory listing: client.file(path).info(), exists(), list() (#145) - #179

Merged
bertysentry merged 2 commits into
mainfrom
145-remote-file-access-24-file-properties-and-directory-listing-clientfilepathinfo-and-clientfiledirlist-with-filters
Sep 24, 2026
Merged

bertysentry merged 2 commits into
mainfrom
145-remote-file-access-24-file-properties-and-directory-listing-clientfilepathinfo-and-clientfiledirlist-with-filters

Conversation

@bertysentry

Copy link
Copy Markdown
Contributor

Closes #145. This is the second step of the remote file access family, after #146 (merged in #177). #147 (download) and #148 (CLI) come next.

What changed

client.file(path) gets metadata and directory listing:

Optional<RemoteFileInfo> info = client.file("C:\\Windows\\Temp\\collect.log").info();   // empty when missing
boolean there = client.file("C:\\Windows\\Temp\\collect.log").exists();

RemoteFileList logs = client.file("C:\\inetpub\\logs").list()
    .glob("*.log").recursive().maxDepth(3).filesOnly()
    .modifiedAfter(Instant.now().minus(Duration.ofDays(1))).minSize(1024L)
    .execute();                       // entries() + inaccessible()

try (Stream<RemoteFileInfo> tree = client.file("D:\\data").list().recursive()
        .onInaccessible(p -> System.err.println("skipped " + p)).stream()) { ... }

New public types (flat in org.metricshub.winrm): RemoteDirectoryListing (the request), RemoteFileList (the result of execute()), and RemoteFileInfo (an immutable value: path, name, size, three timestamps, attribute flags, and the raw attributes()).

How it works

  • A small PowerShell script walks the tree with the .NET DirectoryInfo API and an explicit stack. It never parses dir output and doesn't use Get-ChildItem. It uses EnumerateFileSystemInfos where available and GetFileSystemInfos on PowerShell 2.0 / .NET 2.0.
  • Every filter is evaluated on the host. The filters select what is reported. They never limit which directories are traversed.
  • Reparse points are reported (isReparsePoint()) but never descended into, so a junction loop terminates.
  • A subdirectory that can't be read becomes a ! record and the walk goes on. The directory being listed is the exception: if it can't be read, the listing fails.
  • The record format is the one from the issue. Each entry is one ASCII line: plain integers for attributes, size and FileTimes, and only the path base64-encoded (UTF-8). A malformed or truncated line fails with an exception that names the line.
  • Long paths: absolute paths get the \\?\ prefix (\\?\UNC\ for UNC paths) where .NET accepts it (4.6.2+). The prefix is stripped from the reported paths.

Things a reviewer should know

  • Path cap about 450 characters for info()/list(): the scripts must fit the ~8 KB -EncodedCommand line, and like reads (Remote file access (1/4): foundation + read remote file content — client.file(path), RemoteFiles primitive, whole file, byte ranges, text, streaming #146), a script that doesn't fit is refused rather than uploaded. The walker was compacted to leave room for that path length. A RemoteFileListingTest test guards a 400-character path.
  • Performance: a PowerShell function call costs about 45 µs, so the per-entry record is inlined into both scripts from a single Java constant, RECORD. That made listings about 4× faster: C:\Windows\System32 (16k entries) in 2.3 s instead of 9.5 s on Server 2022, 5.5 s instead of 12.7 s on 2008 R2. All of C:\Windows (126k entries) streams in 15 s.
  • Keepalive: the walker flushes at least every second, sending a bare newline if nothing matched. A selective filter over a big tree therefore doesn't trip stream()'s inactivity timeout.
  • Admins bypass deny ACEs: an administrator's WinRM session has SeBackupPrivilege enabled, and directory enumeration uses backup semantics. The "inaccessible directory" case is therefore tested with a non-elevated local token (RemoteFilesScriptTest). The live test accepts either outcome. This is documented.
  • Refactoring in RemoteFiles: shared start/finish/blocking helpers for reads and metadata (moved out of RemoteFile). PowerShell CLIXML progress records (#< CLIXML …) are now filtered out of failure messages, which also cleans up read errors.
  • Wording changes: "Remote file not found" → "Remote path not found"; the generic failure is "Failed to access remote path"; the timeout message is "Accessing remote path … timed out".
  • Beyond the issue: modifiedBefore and maxSize round out the filters, and maxDepth(n) implies recursive(). onInaccessible receives only the path, as in the issue. The ! record carries the error message, but it isn't exposed.
  • README is unchanged, per AGENTS.md. The feature is documented on the site.

Docs

Tests

  • RemoteFileListingTest (new):
    • the record parser: every field, non-ASCII, > 4 GiB, hidden + system, reparse point, ! records, malformed/truncated lines;
    • glob → regex, FileTime conversion;
    • FakeWsmanServer runs: records split across Receive chunks, an empty directory, a partially inaccessible tree, stream(), info() on a missing path → Optional.empty(), access denied, filters reaching the script, a CLIXML-free error message, command-line fit with long paths.
  • RemoteFilesScriptTest runs the real scripts in local PowerShell 5.1: all fields and timestamps at 100 ns precision, maxDepth, glob (case, brackets, ?), type/size/time filters, a junction loop, a path over 300 characters, an access-denied subdirectory, info(), and the not-found / not-a-directory exit codes.
  • WinRMLiveTest.remoteDirectoryListing passes on Windows Server 2022 (PS 5.1, long path included) and Windows Server 2008 R2 (PS 2.0; the long-path part is skipped because .NET 2.0 rejects \\?\).
  • mvn clean verify site passes locally, with 0 Checkstyle, PMD and SpotBugs findings.

🤖 Generated with Claude Code

Second step of the remote file access family (#146, #145, #147, #148):
client.file(path) gains info() (Optional<RemoteFileInfo>, empty for a
missing path), exists(), and list(), which returns a RemoteDirectoryListing
with glob, recursive, maxDepth, filesOnly/directoriesOnly,
modifiedAfter/modifiedBefore, minSize/maxSize, onInaccessible and timeout,
ending with execute() (RemoteFileList: entries + inaccessible) or stream().

The host walks the tree with a small PowerShell script built on the .NET
DirectoryInfo API and an explicit stack. It uses EnumerateFileSystemInfos
where available and GetFileSystemInfos on PowerShell 2.0 / .NET 2.0.
Every filter is evaluated on the host. Reparse points are reported but
never descended into, so a junction loop terminates. A subdirectory that
cannot be read becomes a "!" record and the walk goes on. Each entry is
one ASCII line: plain integers for attributes, size and FileTimes, and
only the path base64-encoded (UTF-8), so non-ASCII names do not depend
on the console code page. A malformed record fails with a clear exception.

- Long paths: absolute paths get the \\?\ (or \\?\UNC\) prefix where .NET
  accepts it (4.6.2+). The prefix is stripped from the reported paths.
- The per-entry record is inlined instead of calling helper functions.
  A PowerShell function call costs about 45 us, which made large listings
  about 10 times slower (System32 went from 9.5 s to 2.3 s on 2022).
- The script is kept compact so it fits the command line with paths up
  to about 450 characters. Like reads, a script that does not fit is
  refused rather than uploaded.
- RemoteFiles: shared start/finish/blocking helpers for reads and
  metadata. PowerShell CLIXML progress records are filtered out of
  failure messages.
- Docs: files.md (properties, listing, shell vs WMI, limitations), plus
  index.md, preparing-the-host.md, timeouts-and-errors.md,
  migrating-from-winrm4j.md and file-transfers.md.

Tests: record parser and FakeWsmanServer tests (RemoteFileListingTest).
The scripts run in local PowerShell (RemoteFilesScriptTest): depth, glob,
filters, junction loop, a path over 300 characters, an access-denied
subdirectory, non-ASCII names. WinRMLiveTest passes on Windows Server
2022 and 2008 R2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-24T18:39:16.625362Z cf07ede Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a24f8bc4e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/org/metricshub/winrm/RemoteFiles.java Outdated
…t (Codex review)

TickCount is a signed 32-bit millisecond counter, negative for about half
of every 49.7-day cycle. With the clock starting at 0, "TickCount - $t" was
negative on such hosts, so a selective walk that never filled the 32 KB
buffer sent no keepalive and could trip stream()'s inactivity timeout. A
wrap during a walk had the same effect, because PowerShell promotes the
overflowing subtraction to a double instead of wrapping.

The walker now times the silence since its last write with a
System.Diagnostics.Stopwatch: monotonic, 64-bit, and available on .NET 2.0
(PowerShell 2.0). Verified locally: an 89-second walk of C:\Windows that
matched nothing sent 86 keepalives. The live listing test passes on
Windows Server 2008 R2.

files.md: correct the command-line path limits (measured): about 1,150
characters for info(), 450 for list(); the docs had both at 450.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: cf07edeb98

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bertysentry
bertysentry merged commit ecffc73 into main Sep 24, 2026
5 checks passed
@bertysentry
bertysentry deleted the 145-remote-file-access-24-file-properties-and-directory-listing-clientfilepathinfo-and-clientfiledirlist-with-filters branch September 24, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote file access (2/4): file properties and directory listing — client.file(path).info() and client.file(dir).list() with filters

1 participant