Christmas is around the corner. Among the items of interest this year is the Analogue Pocket. The Pocket is an FPGA based device that can hardware emulate a lot of older game consoles along with having some games of its own. I’m getting one prepared for someone else. But I also need to send the device soon to ensure that it arrives at its destination before Christmas. This creates a conflict with getting more games loaded while also shipping it on time. No worries, I can satisfy both if I send the device with something to upload the content.
This is done by a lot of physical game releases, when there is a zero-day “patch” for a game or when the disc is only a license for the game, but the actual game and it’s content are only available online. I’ll be shipping the memory card for the device with an call to action to run the “game installer” on the memory card. After the card is mailed, I can take care of preparing the actual image. The game installer will reach out to my website to find a list of files to download to the memory card, zip files to decompress, or folders to create.
Safety
Though I’m the only one that will be making payloads for my downloader to run, I still imagined some problem scenarios that I wanted to make impossible or more difficult. What if someone were to modify the download so that it were to target writing files to a system directory or some other location? I don’t want this to happen. I’ve made my downloader so that it can only write to the folder in which it lives and to subfolders. The characters that are needed to get to some parent level or to some other drive, if present in the download list, will intentionally cause the application to crash.
Describing an Asset
I started with describing the information that I would need to download an asset. An asset could be a file, a folder, or a zip file. I’ve got an enumeration for gflagging these types.
public enum PayloadType
{
File,
Folder,
ZipFile,
}
Each asset of this type (which I will call a “Payload” from hereon) can be described with the following structure.
public class PayloadInformation
{
[JsonPropertyName("payloadType")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public PayloadType PayloadType { get; set; } = PayloadType.File;
[JsonPropertyName("fileURL")]
public string FileURL { get; set; } = "";
[JsonPropertyName("targetPath")]
public string TargetPath { get; set; } = "";
}
For files and zip archives, the FileURL property contains the URL to the source. The TargetPath property contains a relative path to where this payload item should be downloaded or unzipped to. A download set could have multiple assets. I broke up the files for the the device that I was sending into several Zip files. Sorry, but in the interest of not inundating my site with several people trying this out, I’m not exposing the actual URLs for the assets here. The application will be grabbing a collection of these PayloadInformation items.
public class PayloadInformationList: List<PayloadInformation>
{
public PayloadInformationList() { }
}
The list of assets is placed in a JSON file and made available on a web server.
[
{
"payloadType": "ZipFile",
"fileURL": "https://myserver.com/Pocket.zip",
"targetPath": "",
"versionNumber": "0"
},
{
"payloadType": "ZipFile",
"fileURL": "https://myserver.com/Assets_1.zip",
"targetPath": "Assets",
"versionNumber": "0"
},
{
"payloadType": "ZipFile",
"fileURL": "https://myserver.com/Assets_2.zip",
"targetPath": "Assets",
"versionNumber": "0"
},
{
"payloadType": "ZipFile",
"fileURL": "https://myserver.com/Assets_3.zip",
"targetPath": "Assets",
"versionNumber": "0"
},
{
"payloadType": "ZipFile",
"fileURL": "https://myserver.com/Assets_4.zip",
"targetPath": "Assets",
"versionNumber": "0"
},
{
"payloadType": "Folder",
"targetPath": "Memories/Save States",
"versionNumber": "1"
},
{
"payloadType": "Folder",
"targetPath": "Assets",
"versionNumber": "1"
}
]
I might use some form of this again someday. So I’ve placed the initial URL from which the download list is retrieved in the Application Settings. In the compiled application, the application settings are saved in a JSON file that can be altered with any text editor.
About the Interface
The user interface for this application is using WPF. I grabbed a set of base classes that I often use with WPF applications. It made this using a build of Visual Studio that was just released a month ago that contains significant updates. I found that my base class nolonger works as expected under this new version of Visual Studio. That’s something I will have to tackle another day, as I think that there is a change in the relationship between Linq Expressions and Member Expressions. For now, I just used a subset of the functionality that the classes offerd. Most of the work done by the application can be found in MainViewModel.cs.

To retrieve the list of assets, I have a method named GetPayload() that downloads the JSON containing the list of files and deserializing it. Though I would usually use JSON.Net for serialization needs, I used the System.Text.Json.Serializer for my needs. Here, I also check the paths for characters indicating an attempt to go outside of the application’s root directory and thrown an exception of this occurs.
async Task<List<PayloadInformation>> GetPayloadList()
{
HttpClient client = new HttpClient();
var response = await client.GetAsync(DownloadUrl);
var stringContent = await response.Content.ReadAsStringAsync();
var payloadList = JsonSerializer.Deserialize<List<PayloadInformation>>(stringContent);
payloadList.ForEach(p =>
{
if (!String.IsNullOrEmpty(p.TargetPath))
{
if (p.TargetPath.Contains("..") || p.TargetPath.Contains(":") ||
p.TargetPath.StartsWith("\\") || p.TargetPath.StartsWith("/")
)
{
throw new Exception("Invalid Target Path");
}
}
});
return payloadList;
}
Within MainViewModel::DownloadRoutine() (which runs on a different thread) I step through the payload descriptions one at a time and take action for each one. For folder items, the application just creates the folder (and parent folders if needed). For files, the file is downloaded from the web source to a temporary file on the computer. After it is completely downloaded, it is moved to the final location. This reduces the chance of there being a partially downloaded file on the memory card. The process performed for Zip files is a variation of what is done for files. The zip file is downloaded to a temporary location, and then it is decompressed from that temporary location to its target folder.
while (_downloadQueue.Count > 0)
{
Phase = "Downloading...";
var payload = _downloadQueue.Dequeue();
DownloadProgress = 0;
CurrentPayload = payload;
switch (payload.PayloadType)
{
case PayloadType.File:
{
Phase="Downloading";
var response = client.GetAsync(payload.FileURL).Result;
var content = response.Content.ReadAsByteArrayAsync().Result;
var tempFilePath = Path.Combine(TempFolder, payload.TargetPath);
var fileName = Path.GetFileName(payload.FileURL);
File.WriteAllBytes(tempFilePath, content);
File.Move(tempFilePath, payload.TargetPath, true);
}
break;
case PayloadType.Folder:
{
Phase = "Creating Directory";
var directoryName = payload.TargetPath.Replace('/', Path.DirectorySeparatorChar);
var directoryInfo = new DirectoryInfo(directoryName);
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
}
break;
case PayloadType.ZipFile:
{
WebClient webClient = new WebClient();
webClient.DownloadProgressChanged += DownloadProgressChanged;
webClient.DownloadFileCompleted += WebClient_DownloadFileCompleted;
var tempFilePath = Path.Combine(TempFolder, Path.GetTempFileName()) + ".zip";
var fileName = Path.GetFileName(payload.FileURL);
var directoryName = payload.TargetPath.Replace('/', Path.DirectorySeparatorChar);
if (String.IsNullOrEmpty(directoryName))
{
directoryName = ".";
}
var directoryInfo = new DirectoryInfo(directoryName);
if (!directoryInfo.Exists)
{
directoryInfo.Create();
}
webClient.DownloadFileAsync(new Uri(payload.FileURL), tempFilePath);
_downloadCompleteWait.WaitOne();
Phase = "Decompressing";
System.IO.Compression.ZipFile.ExtractToDirectory(tempFilePath, directoryInfo.FullName,true);
}
break;
default:
break;
}
}
Showing Progress
The download process can take a while. I thought it would be important to make known that the process was progressing. The primary item of feedback shown is a progress bar. As long as it is growing in size, it’s known that data is flowing. I used the WebClient::DownloadProgressChanged event to get updates on how much of a file has been downloaded and updating the progress bar accordingly.
void DownloadProgressChanged(Object sender, DownloadProgressChangedEventArgs e)
{
// Displays the operation identifier, and the transfer progress.
System.Diagnostics.Debug.WriteLine("{0} downloaded {1} of {2} bytes. {3} % complete...",
(string)e.UserState, e.BytesReceived,e.TotalBytesToReceive,e.ProgressPercentage);
DownloadProgress = e.ProgressPercentage;
}
Handling Errors
Theres a good bit of error handling that is missing from this code. I made the decision to do this because of time. Ideally, the program would ensure that it has a connection to the server with the source files. This is different than checking whether there is an Internet connection. The computer having an Internet connection doesn’t imply that it has access to the files. Nor does having access to the files imply generally having access to the Internet. Having used a lot of restricted networks, I’m of the position that just making sure there is an Internet connection too possibly not be sufficient.
It is also possible for a download to be disrupted for a variety of reasons. In addition to detecting this, implementing download resumption would minimize the impact of such occurrences.
If I come back to this application again, I might first problem each of the reasources with an HTTP HEAD requests to see whether they are available. Such a requests would also make known the sizes of the files, which could be used to implement a progress bar for the total progress. Slow downloads, though not an error condition, could be interpreted as an error. Sufficiently informing the user of what’s going on can help prevent it from being thought of as such.
The Code
If you want to grab the code for this and use it for your own purposes, you can find it on GitHub.
https://github.com/j2inet/filedownloader
Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.
Mastodon:ย @j2inet@masto.ai
Instagram:ย @j2inet
Facebook:ย @j2inet
YouTube:ย @j2inet
Telegram:ย j2inet
Twitter:ย @j2inet

























