Table of Contents

Class AtomicDownload

Namespace
McServerLauncher.Services
Assembly
McServerLauncher.dll

Downloads a file so that a failure can never damage what was already on disk.

Writing straight to the destination truncates it the moment the stream opens, long before the download is known to be good. A connection dropped halfway then leaves a valid path holding a half a file — and for the Fabric server jar it was worse than that: the structural check that runs afterwards deletes what it rejects, so an interrupted loader change turned a working server into one with no jar to start at all.

So the bytes go to "<dest>.part", verification runs against that, and only a file that arrived complete and passed its check replaces the real one — in a single filesystem move. This is the same guarantee AtomicJsonFile gives the app's JSON, applied to binaries.

public static class AtomicDownload
Inheritance
AtomicDownload
Inherited Members

Methods

DisplayName(string)

The name to show the user for a file being downloaded to path.

public static string DisplayName(string path)

Parameters

path string

Returns

string

Remarks

Verification runs against the ".part" copy, so the raw file name would tell the user their download of "server.jar.part" failed. This gives back the name they actually recognise.

PathIn(string, string)

Where a file named by a remote server is allowed to land: inside folder, always.

public static string PathIn(string folder, string remoteFileName)

Parameters

folder string
remoteFileName string

Returns

string

Remarks

Path.Combine(folder, name) is not a containment check and does not pretend to be one. Handed "../../evil.jar" it walks up; handed an absolute path it discards the folder entirely and returns that path. The name comes from Modrinth or from GeyserMC's downloads API, so nothing hostile is expected there — which is exactly the reasoning that makes a boundary get skipped once and then five more times.

This exists so the join happens in one place. Every download the app performs goes through it, and a name with no usable last segment is refused rather than guessed at: it can only mean the API said something the app does not understand.

ToFileAsync(HttpContent, string, Func<string, CancellationToken, Task>?, IProgress<double>?, CancellationToken)

Streams content into destPath atomically.

verifyAsync is handed the path of the *temporary* file, so anything it rejects (a checksum mismatch, a jar with the wrong structure) never reaches the real one. It runs with the write handle already closed, because Create(string) opens with FileShare.None and a reader would otherwise hit a sharing violation on Windows. Throwing from it aborts the download and leaves the destination untouched.

public static Task ToFileAsync(HttpContent content, string destPath, Func<string, CancellationToken, Task>? verifyAsync = null, IProgress<double>? progress = null, CancellationToken ct = default)

Parameters

content HttpContent
destPath string
verifyAsync Func<string, CancellationToken, Task>
progress IProgress<double>
ct CancellationToken

Returns

Task