You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Remote file access (3/4): downloadFile(remote, local) — the symmetric, digest-verified counterpart of uploadFile #147
Third issue of the remote file access family: the missing symmetric half of WinRMClient.uploadFile(Path, String). Builds on openStream() from #146 (the foundation) — it does not need the listing of #145.
Context
client.uploadFile(Path localFile, String remoteFile) has existed since the smbj removal (#117): it pushes a local file through the WinRM channel itself, digest-verified, skipping the transfer when the destination already has identical content. There is no way back. Every caller that needs to retrieve a remote log or a command's output file has to shell out to type/Get-Content and hope the encoding survives.
downloadFile is the mirror image, and it should mirror the upload's guarantees, not just its direction: verified integrity, no wasted transfer, and no half-written local file left behind on failure.
Proposed API
Symmetric with the existing method, on the client:
Probe the remote digest with ShellFileCopy.digestProbe / parseAnyDigest.
If the local destination exists with the same digest → no transfer, return.
Otherwise client.file(remote).openStream() → DigestInputStream → temporary file in the destination's directory.
Compare the received digest with the probed one; on match fsync and ATOMIC_MOVE onto the target. On any failure (mismatch, timeout, interruption) delete the temporary file.
A file modified between the probe and the transfer shows up as a digest mismatch — the correct outcome.
Requirements
Digest verification, both ways. Compute the remote digest with the probe ShellFileCopy already has (certutil -hashfile, SHA256 with a SHA1 fallback — see CERTUTIL_ALGORITHMS), compare it with the digest of the received bytes, and fail with the same shape of WindowsRemoteException the upload path raises on a mismatch. Reuse ShellFileCopy.digestHex / parseAnyDigest rather than reimplementing.
Skip an identical transfer, matching the upload's behavior: if the local destination already exists with the same digest, do not transfer, and say so (return value or documented no-op).
Atomic destination: stream into a temporary file in the destination's directory, fsync, then ATOMIC_MOVE onto the target. A failure or a timeout must never leave a truncated file at the destination path.
Bounded memory: built on the streaming read, never readBytes() into a byte[] first. A 500 MB file must download in constant memory (slowly — see below).
Downloads are not resumable — decided, and deliberately out of scope: an interrupted download starts over. Resuming would mean tracking verified byte counts across attempts and re-validating that the remote file has not changed, which is more state machine than this transport's speed justifies. Say so in the Javadoc and in file-transfers.md so callers do not expect otherwise.
Reuse the quota-rejection retry (isRetryableQuotaRejection, QUOTA_RETRIES, QUOTA_RETRY_DELAY_MILLIS): a long download hits the same WinRM operation quotas the upload does.
Directory destination: downloadFile(remote, Path.of("C:\\dir")) where the local path is an existing directory writes dir\<remote file name>, like cp — decided (the CLI get default destination in Remote file access (4/4): CLI ls, stat, cat and get subcommands #148 needs the same resolution). Document it.
Honest performance documentation. Base64 through a command shell is roughly an order of magnitude slower than SMB; publish a measured figure from the live host in file-transfers.md and keep the existing "not a bulk transport" caveat prominent. Callers moving gigabytes should use SMB, and the docs should say so.
Timeout is the client's wall-clock deadline for the blocking method; a large download will need an explicitly raised timeout, and the exception message must make the cause obvious (bytes transferred / total when it fired).
Tests & docs
Round-trip test: uploadFile then downloadFile returns byte-identical content, for a binary file with every byte value and for a file with a non-ASCII name.
FakeWsmanServer tests: digest mismatch → failure with no file at the destination; interruption mid-transfer → no partial file left behind and no half-written temporary file; identical-digest destination → no transfer performed.
WinRMLiveTest: download a real file from anaxagore and verify its digest.
file-transfers.md extended with the download direction, the integrity and atomicity guarantees, the measured throughput, and the SMB recommendation for bulk data; README snippet next to uploadFile.
Acceptance criteria
downloadFile retrieves byte-exact content, digest-verified against the remote host.
No partial file is ever visible at the destination path — verified by a test that interrupts mid-transfer.
An identical local file is not re-downloaded.
A file substantially larger than the JVM heap downloads successfully.
mvn verify site green: no checkstyle/PMD/SpotBugs findings, full Javadoc, docs updated.
Context
client.uploadFile(Path localFile, String remoteFile)has existed since the smbj removal (#117): it pushes a local file through the WinRM channel itself, digest-verified, skipping the transfer when the destination already has identical content. There is no way back. Every caller that needs to retrieve a remote log or a command's output file has to shell out totype/Get-Contentand hope the encoding survives.downloadFileis the mirror image, and it should mirror the upload's guarantees, not just its direction: verified integrity, no wasted transfer, and no half-written local file left behind on failure.Proposed API
Symmetric with the existing method, on the client:
And as a terminal on the per-path request, for the fluent form:
Flow — one remote digest probe
ShellFileCopy.digestProbe/parseAnyDigest.client.file(remote).openStream()→DigestInputStream→ temporary file in the destination's directory.fsyncandATOMIC_MOVEonto the target. On any failure (mismatch, timeout, interruption) delete the temporary file.A file modified between the probe and the transfer shows up as a digest mismatch — the correct outcome.
Requirements
ShellFileCopyalready has (certutil -hashfile, SHA256 with a SHA1 fallback — seeCERTUTIL_ALGORITHMS), compare it with the digest of the received bytes, and fail with the same shape ofWindowsRemoteExceptionthe upload path raises on a mismatch. ReuseShellFileCopy.digestHex/parseAnyDigestrather than reimplementing.fsync, thenATOMIC_MOVEonto the target. A failure or a timeout must never leave a truncated file at the destination path.readBytes()into abyte[]first. A 500 MB file must download in constant memory (slowly — see below).file-transfers.mdso callers do not expect otherwise.isRetryableQuotaRejection,QUOTA_RETRIES,QUOTA_RETRY_DELAY_MILLIS): a long download hits the same WinRM operation quotas the upload does.downloadFile(remote, Path.of("C:\\dir"))where the local path is an existing directory writesdir\<remote file name>, likecp— decided (the CLIgetdefault destination in Remote file access (4/4): CLI ls, stat, cat and get subcommands #148 needs the same resolution). Document it.file-transfers.mdand keep the existing "not a bulk transport" caveat prominent. Callers moving gigabytes should use SMB, and the docs should say so.Tests & docs
uploadFilethendownloadFilereturns byte-identical content, for a binary file with every byte value and for a file with a non-ASCII name.FakeWsmanServertests: digest mismatch → failure with no file at the destination; interruption mid-transfer → no partial file left behind and no half-written temporary file; identical-digest destination → no transfer performed.WinRMLiveTest: download a real file fromanaxagoreand verify its digest.file-transfers.mdextended with the download direction, the integrity and atomicity guarantees, the measured throughput, and the SMB recommendation for bulk data; README snippet next touploadFile.Acceptance criteria
downloadFileretrieves byte-exact content, digest-verified against the remote host.mvn verify sitegreen: no checkstyle/PMD/SpotBugs findings, full Javadoc, docs updated.🤖 Generated with Claude Code