MetricsHub / MetricsHub/winrm-java
Remote file access (2/4): read remote file content — whole file, byte ranges (offset/length), text with charset, streaming
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 11
- Forks
- 4
- Avg merge
- 5d 5h
- Merged PRs (30d)
- 6
Description
Second issue of the remote file access family, building on the
client.file(path)entry point, theRemoteFileInfotype and the base64-framed PowerShell helper introduced by #145.
Context
Listing tells you a remote file is there; this issue reads its bytes. Same channel as the upload path in ShellFileCopy, run in reverse: the remote side base64-encodes the requested byte range, the client decodes it as the output chunks arrive.
Two properties make base64 the right framing rather than an accident of history:
- it is binary-safe — arbitrary bytes survive a channel that is fundamentally a text console, with none of the code page hazards of #142; and
- it is ASCII-only, so the client never has to know or guess the remote console encoding to recover the exact bytes.
The cost is the 4/3 inflation plus the shell's own overhead: this is a mechanism for configuration files, logs and small data files, not a bulk transport. That must be stated in the API docs, exactly as the upload path already states it.
Proposed API
Terminals on the client.file(path) request:
// Whole file, bytes
byte[] content = client.file("C:\\Windows\\Temp\\collect.bin").readBytes();
// Whole file, text with an explicit charset (no guessing, no default)
String text = client.file("C:\\inetpub\\logs\\u_ex260729.log")
.readText(StandardCharsets.UTF_8);
// A range: from a position, for a length — the point of the exercise for big logs
byte[] tail = client.file("D:\\logs\\huge.log")
.offset(1_073_741_824L) // absolute byte position
.length(65_536) // bytes to read; omit for "to the end"
.readBytes();
// Streaming, so memory stays bounded by the buffer and not by the file
try (InputStream in = client.file("D:\\logs\\huge.log").openStream()) {
...
}
try (BufferedReader reader = client.file("D:\\logs\\huge.log")
.charset(StandardCharsets.UTF_8)
.openReader()) {
reader.lines().filter(l -> l.contains("ERROR")).forEach(System.out::println);
}
// Integrity, reusing the digest probe ShellFileCopy already has
String sha256 = client.file("C:\\Windows\\Temp\\collect.ps1").digest("SHA256");
offset(long)/length(long) compose with every read terminal.
Negative offsets mean "from the end" — decided, and part of the scope:
// The tail case: last 8 KiB of a log, one seek, no scan
byte[] tail = client.file("D:\\logs\\huge.log").offset(-8192).readBytes();
Exact semantics to implement and document:
offset(-n)resolves server-side tomax(0, size - n), so a file shorter thannreturns the whole file, never an error and never a short read from a negative position.length(...)still applies after the resolution and still clamps at EOF:offset(-8192).length(1024)returns the first 1 KiB of the last 8 KiB.- The size is read inside the same remote invocation as the seek (from the open
FileStream), not by a separateinfo()round trip — otherwise a growing log would be tailed from a stale position. offset(0)is the start of the file; there is no "negative zero" case to special-case.
Requirements
The remote read
- Open with a share mode that tolerates other writers:
[IO.File]::Open($p, 'Open', 'Read', 'ReadWrite'), not[IO.File]::OpenRead. A log being written by a running service is the single most common thing a caller wants to read, andOpenReadfails on it. Files held with a truly exclusive lock (pagefile.sys, live registry hives) still fail — map that to a clear exception naming the sharing violation. - Seek + bounded read, so a range costs a seek and not a full scan: position the stream at
offset, read at mostlengthbytes, base64 the buffer, write it out. Never read the whole file to serve a range. - Chunked, streaming output: the script writes base64 in fixed-size blocks so the client can decode incrementally through the streaming command terminal (#111) instead of buffering the whole payload. Keep each write within the wire framing the shell already handles, and mind
MaxEnvelopeSize(153 600) for the command itself, not just the output. -EncodedCommandinvocation and pure-ASCII output, exactly as in #145 — nocmd.exequoting of caller-supplied paths, no dependence on the console code page.certutil -encodefallback for whole-file reads when PowerShell is unavailable or constrained (strip the-----BEGIN CERTIFICATE-----framing it adds). Ranged reads have nocertutilequivalent: fail with a message that says so.
Semantics to pin down and document
- Ranges are byte ranges, not character ranges. A range boundary can split a multi-byte character, so
readText(Charset)on a ranged read may yieldU+FFFDat the edges. Say this in the Javadoc; do not try to be clever about it. - No BOM stripping in
readBytes(). ForreadText/openReader, either strip a leading BOM or don't — pick one, document it, test it. offsetpast EOF returns empty, not an error;lengthbeyond EOF returns what exists. A file that grows or shrinks between two ranged reads is the caller's problem, but note it: this is not a snapshot.- Empty file, zero
length, and 0-byte range are valid and return an empty result. - Size guard: an unbounded
readBytes()on a multi-gigabyte file must not OOM the JVM silently. Enforce a documented default cap (e.g. 64 MiB) with an explicitmaxBytes(long)override, and point callers atopenStream()/downloadFilefor anything larger. - Throughput expectation documented with a measured figure from the live host, next to the existing "designed for small script files, not bulk data" wording in
file-transfers.md. - Timeout semantics follow the existing split: the blocking terminals use the wall-clock deadline;
openStream()/openReader()use the inactivity semantics of the streaming terminals.
Tests & docs
FakeWsmanServertests: whole-file read, ranged read (mid-file, at EOF, past EOF, zero length), negative offset (larger than, equal to and smaller than the file size, and combined withlength), binary content with every byte value 0–255, a multi-byte character split across two chunks (must decode correctly through the incremental path), the size cap, and the sharing-violation and missing-file failures.- Digest agreement test:
readBytes()of a file uploaded withuploadFilematches the local bytes, anddigest("SHA256")matches the local digest. WinRMLiveTest: read a real file whole, read a range from a large file (assert the range is served by a seek, i.e. duration does not scale with the file size), and read a file currently held open by another process.files.mdgains the read section; README gains the snippet;file-transfers.mdcross-links the reverse direction.
Acceptance criteria
- Whole-file and ranged reads return byte-exact content for binary and text files, verified by digest against the local original.
- A range near the end of a large file is served without transferring the whole file, both with an absolute offset and with
offset(-n). - A log file open for writing by another process can be read.
- The default size cap prevents an accidental unbounded read, and
openStream()reads a file larger than the cap with bounded memory. mvn verify sitegreen: no checkstyle/PMD/SpotBugs findings, full Javadoc, docs updated.
🤖 Generated with Claude Code
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the client.file(path) entry point and review RemoteFileInfo, ShellFileCopy, the streaming command terminal from #111, and the base64 helper from #145. Use FakeWsmanServer and WinRMLiveTest as the primary test entry points, then update files.md, README, and file-transfers.md. Done means whole-file, ranged, negative-offset, streaming, digest, failure, cap, and live-host cases pass with mvn verify site.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, powershell
- Domain
- api, backend-api-design, networking
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100