ProjectDDD/Packages/com.singularitygroup.hotreload/Editor/Installation/DownloadUtility.cs
2025-07-08 19:46:31 +09:00

99 lines
4.9 KiB
C#

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using SingularityGroup.HotReload.Editor.Cli;
namespace SingularityGroup.HotReload.Editor {
static class DownloadUtility {
const string baseUrl = "https://cdn.hotreload.net";
public static async Task<DownloadResult> DownloadFile(string url, string targetFilePath, IProgress<float> progress, CancellationToken cancellationToken) {
var tmpDir = Path.GetDirectoryName(targetFilePath);
Directory.CreateDirectory(tmpDir);
using(var client = HttpClientUtils.CreateHttpClient()) {
client.Timeout = TimeSpan.FromMinutes(10);
return await client.DownloadAsync(url, targetFilePath, progress, cancellationToken).ConfigureAwait(false);
}
}
public static string GetPackagePrefix(string version) {
if (PackageConst.IsAssetStoreBuild) {
return $"releases/asset-store/{version.Replace('.', '-')}";
}
return $"releases/{version.Replace('.', '-')}";
}
public static string GetDownloadUrl(string key) {
return $"{baseUrl}/{key}";
}
public static async Task<DownloadResult> DownloadAsync(this HttpClient client, string requestUri, string destinationFilePath, IProgress<float> progress, CancellationToken cancellationToken = default(CancellationToken)) {
// Get the http headers first to examine the content length
using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)) {
if (response.StatusCode != HttpStatusCode.OK) {
throw new DownloadException($"Download failed with status code {response.StatusCode} and reason {response.ReasonPhrase}");
}
var contentLength = response.Content.Headers.ContentLength;
if (!contentLength.HasValue) {
throw new DownloadException("Download failed: Content length unknown");
}
using (var fs = new FileStream(destinationFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
using (var download = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) {
// Ignore progress reporting when no progress reporter was
if (progress == null) {
await download.CopyToAsync(fs).ConfigureAwait(false);
} else {
// Convert absolute progress (bytes downloaded) into relative progress (0% - 99.9%)
var relativeProgress = new Progress<long>(totalBytes => progress.Report(Math.Min(99.9f, (float)totalBytes / contentLength.Value)));
// Use extension method to report progress while downloading
await download.CopyToAsync(fs, 81920, relativeProgress, cancellationToken).ConfigureAwait(false);
}
await fs.FlushAsync().ConfigureAwait(false);
if (fs.Length != contentLength.Value) {
throw new DownloadException("Download failed: download file is corrupted");
}
return new DownloadResult(HttpStatusCode.OK, null);
}
}
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken) {
if (source == null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new ArgumentException("Has to be readable", nameof(source));
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new ArgumentException("Has to be writable", nameof(destination));
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
var buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
[Serializable]
public class DownloadException : ApplicationException {
public DownloadException(string message)
: base(message) {
}
public DownloadException(string message, Exception innerException)
: base(message, innerException) {
}
}
}
}