Hey there! I am currently maintaining a mod launcher software that utilizes the Dropbox .NET C# API to allow users to download mod files.
Unfortunately, large files (usually 2-5GB in size) do not download completely while using the launcher, hanging mid-way through the download and refusing to complete, even if a download is restarted promptly. However, this issue only happens for a select few people who are using the launcher software, which makes it appear as though Dropbox is enforcing a quota or limit on the number of concurrent downloads that are able to be processed at a time. This would explain why, for one particular user, the download stopped at around 45%, while for another, it hung at roughly 65%.
I have had a conversation with someone who seemed to be affiliated with the .NET C# API team prior to making this post a few months back, named Hubert, and they suggested using range requests to tackle the issue. However, not even range requests seemed to do the trick, as a given download would end up becoming stuck yet again after some time, with stream-related errors bubbling up for no meaningful reason.
I am fully well-aware that Dropbox is NOT intended to serve as a content-delivery network, however the support provided for the .NET C# API is somewhat ridiculous.
Currently, I am using V2 of the Dropbox .NET SDK, Version 6.36.0. Line 46 is where an unexpected stream error is occurring within the downloader code (included in this post), wherein the code attempts to read data from the Stream object returned by a single DownloadAsync() API call from the DropboxClient class before the do while loop begins (the DownloadAsync() function is coherently abstracted as DownloadFile() in a wrapper class for the DropboxClient named DropboxFileManager, to interact with the API more efficiently). The error reads the following: Unable to read data from the transport connection: The connection was closed.
Attached are some samples of code currently being used in my launcher software related to downloading. I would really appreciate it if anyone could shed some slight on this:
DropboxClient Custom HttpClient Configuration:
public DropboxFileManager()
{
var configManager = new LauncherConfigManager();
RefreshToken = configManager.LauncherConfig[DropboxRefreshTokenKey]?.ToString();
ClientId = configManager.LauncherConfig[DropboxClientIdKey]?.ToString();
ClientSecret = configManager.LauncherConfig[DropboxClientSecretKey]?.ToString();
var httpClient = new HttpClient(new WebRequestHandler { ReadWriteTimeout = 10 * 1000 }) { Timeout = TimeSpan.FromMinutes(1000) };
var clientConfig = new DropboxClientConfig($"{NativeManifest.ExecutableAppName}") { HttpClient = httpClient };
_client = new DropboxClient(RefreshToken, ClientId, ClientSecret, clientConfig);
}
Downloader code (no need to tell me that this needs refactoring, I am well-aware that it does):
private async Task<LibraryItemCard> DownloadModFileAndUpdateCardStatus(LibraryItemCard card, Mod mod, string uri, string localPath)
{
mod.IsBusy = true;
await Task.Run(async () =>
{
while (mod.IsBusy)
{
mod.IsQueuing = true;
card = await UpdateItemCardProgress(card, mod);
mod.IsQueuing = false;
IDownloadResponse<FileMetadata> response = await _fileManager.DownloadFile(uri);
if (response == null)
{
mod.IsReconnecting = true;
card = await UpdateItemCardProgress(card, mod);
mod.IsReconnecting = false;
await Task.Delay(4000);
continue;
}
Stream stream = await response.GetContentAsStreamAsync();
var modFileZipPath = $"{localPath}.zip";
FileStream fileStream;
try
{
fileStream = File.OpenWrite(modFileZipPath);
}
catch
{
ShowErrorDialog(ZipAccessError);
continue;
}
var prevProgress = 0;
int streamBufferLength;
var resumeTask = false;
do
{
if (mod.IsCancellingDownload || !IsCurrentPage(MainPageName) && !IsCurrentPage(SettingsPageName))
{
fileStream.Close();
stream.Close();
if (!mod.IsCancellingDownload) return card;
if (File.Exists(modFileZipPath)) File.Delete(modFileZipPath);
mod.IsBusy = false;
await RunBackgroundAction(() => card = Refre**bleep**emCard(card, mod));
mod.IsCancellingDownload = false;
return card;
}
var streamBuffer = new byte[1024];
try
{
streamBufferLength = await stream.ReadAsync(streamBuffer, 0, 1024);
}
catch
{
fileStream.Close();
resumeTask = true;
break;
}
try
{
fileStream.Write(streamBuffer, 0, streamBufferLength);
}
catch
{
ShowErrorDialog(NotEnoughSpaceError);
return card;
}
mod.Progress = (int)((double)fileStream.Length / response.Response.Size * 100);
if (mod.Progress == prevProgress) continue;
prevProgress = mod.Progress;
if (!mod.IsOptionsButtonActivated) card = await UpdateItemCardProgress(card, mod);
} while (stream.CanRead && streamBufferLength > 0);
if (resumeTask) continue;
fileStream.Close();
card = await ExtractModAndUpdateCardStatus(card, mod, modFileZipPath);
await RunBackgroundAction(() => card = Refre**bleep**emCard(card, mod));
mod.IsBusy = false;
if (mod.IsUpdated) mod.Configure(FocusedGame);
}
return card;
});
return card;
}
Again, any insight is much appreciated.