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

Aperta
#145 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Funzionalità
Chiarezza
Abbastanza chiara
Stato di attività
Tranquilla
Stack tecnologico
java, powershell

Direzione di ricerca

Inizia leggendo ShellFileCopy per il framing dei comandi e il rafforzamento del trasporto, quindi WqlRequest.stream per il terminale di streaming; usa i test di FakeWsmanServer e WinRMLiveTest come punti di ingresso per i test. Il lavoro è completo quando i metadati dei file e gli elenchi filtrati eseguono il round-trip in modo sicuro, vengono segnalati i percorsi inaccessibili, vengono evitati i cicli dei reparse points e mvn verify site ha esito positivo, con files.md collegato da index.md e la README aggiornata.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

First issue of a remote file access family: metadata and listing here, content reading in #146, downloadFile in #147, CLI subcommands in #148. This one introduces the shared foundation (the client.file(...) entry point, the RemoteFileInfo type, and the base64-framed PowerShell helper) that the other three build on.

Context

WinRM has no file-access verb of its own — nothing like SFTP's OPENDIR/READ. Reading files on a remote host over WinRM therefore means reusing one of the two channels the client already speaks:

  1. WMI/WQLCIM_DataFile and CIM_Directory expose real metadata (FileSize, LastModified, CreationDate, Readable, Hidden, System, Archive, Extension), and the client's WQL layer already supports the association form needed to list a directory:

    SELECT Name, FileSize, LastModified FROM ASSOCIATORS OF {Win32_Directory.Name="C:\\Windows\\Temp"} WHERE ResultClass = CIM_DataFile
    

    Workable, but: the WMI file provider is notoriously slow (it walks the filesystem inside WMI, and an unindexed CIM_DataFile query on a large tree can take minutes), recursion means one association query per directory, timestamps come back as CIM DMTF datetimes, and it cannot read content at all.

  2. The command shell — the channel ShellFileCopy already uses to upload files (chunked base64 through cmd.exe, decoded with certutil, digest-verified). Listing and reading are the same trick in reverse, and it is fast, recursive, and filters server-side.

This family takes the shell path and keeps WMI as a documented alternative for metadata-only cases. That keeps the zero-dependency promise, needs no extra port, and reuses the transport hardening (quota retries, envelope chunking, incremental decoding) already in the codebase.

Proposed API

A per-path entry point, sibling of wql(...) / command(...):

try (WinRMClient client = WinRMClient.builder("server01.acme.com")
        .credentials("ACME\\admin", password)
        .build()) {

    // One path: existence and properties
    if (client.file("C:\\Windows\\Temp\\collect.log").exists()) {
        RemoteFileInfo info = client.file("C:\\Windows\\Temp\\collect.log").info().orElseThrow();
        System.out.println(info.size() + " bytes, modified " + info.lastModified());
    }

    // A directory: listing with filters
    List<RemoteFileInfo> logs = client.list("C:\\inetpub\\logs")
        .glob("*.log")                                  // server-side -Filter
        .recursive()                                    // off by default
        .maxDepth(3)
        .filesOnly()                                    // or directoriesOnly()
        .modifiedAfter(Instant.now().minus(Duration.ofDays(1)))
        .minSize(1024L)
        .execute();

    // Large trees stream, like WQL rows
    try (Stream<RemoteFileInfo> tree = client.list("D:\\data").recursive().stream()) {
        tree.filter(RemoteFileInfo::isDirectory).forEach(System.out::println);
    }
}

RemoteFileInfo — an immutable value type in the style of WqlRow (Java 11, so a final class with accessors, not a record):

accessor source
String path() full path as reported by the host
String name() last path element
boolean isDirectory() Attributes
long size() Length (0 for directories)
Instant lastModified(), created(), lastAccessed() *TimeUtc
boolean isHidden(), isSystem(), isReadOnly(), isArchive(), isReparsePoint() Attributes bit flags
int attributes() raw FileAttributes value, for anything not exposed above

Requirements

The remote helper — base64-framed PowerShell, not dir
  • Never parse dir output: its column layout, date format and decimal separator are locale-dependent, and it has no timestamps beyond minute granularity. Use PowerShell (Get-ChildItem / [IO.FileInfo]).
  • Invoke it with powershell -NoProfile -NonInteractive -EncodedCommand <base64 UTF-16LE>. This sidesteps cmd.exe quoting entirely (no ^/"/% escaping games with user-supplied paths) and is immune to the console code page problems fixed in #142. Respect the existing MAX_COMMAND_LENGTH (8000) budget from ShellFileCopy: the scripts must stay terse; if one grows past the budget, upload it as a script file with the existing ShellFileCopy path and invoke it by name.
  • The script emits pure ASCII: it builds its records as UTF-8 bytes and writes them back as base64, so non-ASCII file names survive whatever the remote console encoding is. The client decodes base64 → UTF-8 → records. No charset guessing anywhere in this path.
  • Locale-independent field encoding inside the records: timestamps as .Ticks (UTC), sizes and attributes as plain integers, one record per line, tab-delimited with the path last. Windows forbids control characters (0x00–0x1F) in file names, so tab and newline are safe delimiters — state that reasoning in the code comment.
  • PowerShell may be unavailable or constrained (AppLocker, Constrained Language Mode, PowerShell removed). Detect the failure and report it as a clear WinRMClientException naming the cause, instead of surfacing a raw cmd.exe error. Document the WMI/ASSOCIATORS OF alternative in that case.
Behavior
  • Access-denied entries must not be silently dropped: Get-ChildItem on a tree with protected subdirectories partially fails. Collect those paths and expose them (e.g. RemoteFileList.inaccessible() from execute(), and a onInaccessible(Consumer<String>) hook for stream()) so a caller can tell "no matches" from "could not look".
  • Do not follow reparse points/junctions/symlinks when recursing (cycle risk, and C:\Documents and Settings-style legacy junctions loop straight back). Expose them as entries with isReparsePoint() true.
  • Filters are pushed server-side where PowerShell supports it cheaply (-Filter for the glob, -Recurse, -Depth, -File/-Directory); size and time predicates are applied by the script (Where-Object) so a big tree is not shipped over the wire just to be discarded locally. Document which filter is evaluated where.
  • glob(String) is a Windows wildcard pattern (*, ?), not a regex and not a full path — document that -Filter uses the legacy 8.3-aware matcher, so *.log also matches x.log1-style short names; offer regex(String) only if a follow-up needs it.
  • stream() builds on the streaming command terminal of #111: records are parsed and yielded as the output chunks arrive, memory bounded by the parse buffer, not by the tree size. Must be closed (try-with-resources) like WqlRequest.stream().
  • info() returns Optional.empty() for a missing path — a non-existent file is not an error. Genuine failures (access denied on the path itself, invalid path, unreachable) throw.
  • UNC paths (\\server\share\...) work when the caller's credentials can reach them, i.e. only with credential delegation (#141) — document the second-hop limitation rather than pretending it works.
  • Long paths (>260 chars) must not silently truncate: use the \\?\-aware .NET APIs and cover one such path in the live test.
Tests & docs
  • FakeWsmanServer-based tests: scripted base64 record payloads → parsed RemoteFileInfo values, including a non-ASCII name, a >4 GB size, a hidden+system entry, a reparse point, an empty directory, and a partially-inaccessible tree.
  • Record-parser unit tests for malformed/truncated payloads (must fail with a clear exception, never with a half-populated object).
  • WinRMLiveTest coverage against a real host (anaxagore): list C:\Windows\Temp, a recursive listing with maxDepth, and a non-ASCII file name.
  • New src/site/markdown/files.md page (remote file access: listing, properties, the shell-vs-WMI trade-off, limitations) linked from index.md; README gets the one-liner + snippet.

Acceptance criteria

  • client.file(path).exists()/.info() and client.list(dir) with all documented filters work against a real host and against FakeWsmanServer.
  • Non-ASCII file names round-trip correctly regardless of the remote machine's code page.
  • A recursive listing of a tree containing an access-denied subdirectory returns the readable entries and reports the inaccessible paths.
  • Recursion terminates on a junction loop.
  • mvn verify site green: no checkstyle/PMD/SpotBugs findings, full Javadoc, files.md published.

🤖 Generated with Claude Code

Lingua principale
Java
Stelle
11
Fork
4
Merge medio
5g 5h
PR unite (30g)
6

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di MetricsHub/winrm-java

Tutte le issue di MetricsHub/winrm-java

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.