/// <summary> /// Initializes a new instance of the <see cref="DownloadedAsset" /> class. /// </summary> /// <param name="fileName">Name of the file.</param> /// <param name="apiAsset"></param> /// <param name="info"></param> /// <param name="stream">The stream.</param> /// <exception cref="ArgumentNullException">fileName or stream is null.</exception> public DownloadedAsset(string fileName, ApiAsset apiAsset, DownloadInfo info, Stream stream) { if (fileName == null) throw new ArgumentNullException(nameof(fileName)); if (apiAsset == null) throw new ArgumentNullException(nameof(apiAsset)); if (stream == null) throw new ArgumentNullException(nameof(stream)); FileName = fileName; ApiAsset = apiAsset; Info = info; Stream = stream; }
/// <summary> /// Downloads the asset. /// </summary> /// <param name="asset">The asset.</param> /// <returns></returns> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="Exception">the specified asset id is invalid</exception> public async Task<IDownloadedAsset> DownloadAsset(ApiAsset asset) { if (asset == null) throw new ArgumentNullException(nameof(asset)); var downloadInfo = ResolveDownloadInfo(asset); using (var webClient = _webClientFactory.CreateWebClient(asset.Type != AssetType.Mod)) { using (var stream = await webClient.OpenReadTaskAsync(downloadInfo.Url)) { // Read content information from the headers. var contentDispositionHeader = webClient.ResponseHeaders.Get("Content-Disposition"); var contentLengthHeader = webClient.ResponseHeaders.Get("Content-Length"); var contentTypeHeader = webClient.ResponseHeaders.Get("Content-Type"); // Ensure the required content headers exist. if (string.IsNullOrWhiteSpace(contentDispositionHeader) || string.IsNullOrWhiteSpace(contentTypeHeader)) throw new Exception("invalid headers"); // Parse the content length header to an integer. var contentLength = 0; if (contentLengthHeader != null && !int.TryParse(contentLengthHeader, out contentLength)) throw new Exception("invalid headers"); // Get asset information for the asset type specified in the url. var assetInfo = asset.Type.GetCustomAttribute<AssetInfoAttribute>(); // Ensure the type of the received content matches the expected content type. if (assetInfo == null || assetInfo.ContentType != contentTypeHeader.Split(';').FirstOrDefault()) throw new Exception("invalid response type"); // Extract the filename of the asset from the content disposition header. var fileNameMatch = Regex.Match(contentDispositionHeader, @"attachment; filename=(""?)(.*)\1"); if (fileNameMatch == null || !fileNameMatch.Success) throw new Exception("invalid headers"); var fileName = fileNameMatch.Groups[2].Value; // Copy the contents of the downloaded stream to a memory stream. var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream); // Verify we received all content. if (contentLengthHeader != null && memoryStream.Length != contentLength) throw new Exception("unexpected end of stream"); // Create an instance of ParkitectAsset with the received content and data. return new DownloadedAsset(fileName, asset, downloadInfo, memoryStream); } } }
private DownloadInfo ResolveDownloadInfo(ApiAsset asset) { if (asset == null) throw new ArgumentNullException(nameof(asset)); switch (asset.Type) { case AssetType.Blueprint: case AssetType.Savegame: return new DownloadInfo(asset.Resource.Data.Url, null, null); case AssetType.Mod: var repoUrl = asset.Resource.Data.Source; var repoUrlParts = repoUrl.Split("/".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var repo = $"{repoUrlParts[repoUrlParts.Length - 2]}/{repoUrlParts[repoUrlParts.Length - 1]}"; if (asset.Resource.Data.ZipBall == null) throw new Exception("mod has not yet been released(tagged)"); return new DownloadInfo(asset.Resource.Data.ZipBall, repo, asset.Resource.Data.Version); default: throw new Exception("unsupported mod type"); } }