diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs
index 1f89ab42df..774a904b8f 100644
--- a/Core/Extensions/IOExtensions.cs
+++ b/Core/Extensions/IOExtensions.cs
@@ -54,5 +54,28 @@ public static DriveInfo GetDrive(this DirectoryInfo dir)
.OrderByDescending(dr => dr.RootDirectory.FullName.Length)
.FirstOrDefault();
+ ///
+ /// A version of Stream.CopyTo with progress updates.
+ ///
+ /// Stream from which to copy
+ /// Stream to which to copy
+ /// Callback to notify as we traverse the input, called with count of bytes received
+ public static void CopyTo(this Stream src, Stream dest, IProgress progress)
+ {
+ // CopyTo says its default buffer is 81920, but we want more than 1 update for a 100 KiB file
+ const int bufSize = 8192;
+ var buffer = new byte[bufSize];
+ long total = 0;
+ while (true)
+ {
+ var bytesRead = src.Read(buffer, 0, bufSize);
+ if (bytesRead == 0)
+ {
+ break;
+ }
+ dest.Write(buffer, 0, bytesRead);
+ progress.Report(total += bytesRead);
+ }
+ }
}
}
diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs
index 97d97748cb..d6e78843af 100644
--- a/Core/ModuleInstaller.cs
+++ b/Core/ModuleInstaller.cs
@@ -155,20 +155,11 @@ public void InstallList(ICollection modules, RelationshipResolverOpt
foreach (CkanModule module in modsToInstall)
{
+ User.RaiseMessage(" * {0}", Cache.DescribeAvailability(module));
if (!Cache.IsMaybeCachedZip(module))
{
- User.RaiseMessage(" * {0} {1} ({2}, {3})",
- module.name,
- module.version,
- module.download.Host,
- CkanModule.FmtSize(module.download_size)
- );
downloads.Add(module);
}
- else
- {
- User.RaiseMessage(Properties.Resources.ModuleInstallerModuleCached, module.name, module.version);
- }
}
if (ConfirmPrompt && !User.RaiseYesNoDialog("Continue?"))
@@ -1053,12 +1044,21 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa
{
if (!Cache.IsMaybeCachedZip(module))
{
- User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeInstallingUncached,
- module.name,
- module.version,
- module.download.Host,
- CkanModule.FmtSize(module.download_size)
- );
+ var inProgressFile = new FileInfo(Cache.GetInProgressFileName(module));
+ if (inProgressFile.Exists)
+ {
+ User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeInstallingResuming,
+ module.name, module.version,
+ module.download.Host,
+ CkanModule.FmtSize(module.download_size - inProgressFile.Length));
+ }
+ else
+ {
+ User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeInstallingUncached,
+ module.name, module.version,
+ module.download.Host,
+ CkanModule.FmtSize(module.download_size));
+ }
}
else
{
@@ -1086,13 +1086,19 @@ public void Upgrade(IEnumerable modules, IDownloader netAsyncDownloa
{
if (!Cache.IsMaybeCachedZip(module))
{
- User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeUpgradingUncached,
- module.name,
- installed.version,
- module.version,
- module.download.Host,
- CkanModule.FmtSize(module.download_size)
- );
+ var inProgressFile = new FileInfo(Cache.GetInProgressFileName(module));
+ if (inProgressFile.Exists)
+ {
+ User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeUpgradingResuming,
+ module.name, installed.version, module.version,
+ module.download.Host, CkanModule.FmtSize(module.download_size - inProgressFile.Length));
+ }
+ else
+ {
+ User.RaiseMessage(Properties.Resources.ModuleInstallerUpgradeUpgradingUncached,
+ module.name, installed.version, module.version,
+ module.download.Host, CkanModule.FmtSize(module.download_size));
+ }
}
else
{
diff --git a/Core/Net/Net.cs b/Core/Net/Net.cs
index c178297b69..037eaf18a6 100644
--- a/Core/Net/Net.cs
+++ b/Core/Net/Net.cs
@@ -6,9 +6,11 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
+
using Autofac;
using ChinhDo.Transactions.FileManager;
using log4net;
+
using CKAN.Configuration;
namespace CKAN
@@ -25,7 +27,7 @@ public class Net
private const int MaxRetries = 3;
private const int RetryDelayMilliseconds = 100;
- private static readonly ILog log = LogManager.GetLogger(typeof(Net));
+ private static readonly ILog log = LogManager.GetLogger(typeof(Net));
public static readonly Dictionary ThrottledHosts = new Dictionary()
{
@@ -355,109 +357,5 @@ public static Uri GetRawUri(Uri remoteUri)
return remoteUri;
}
}
-
- ///
- /// A WebClient with some CKAN-sepcific adjustments:
- /// - A user agent string (required by GitHub API policy)
- /// - Sets the Accept header to a given MIME type (needed to get raw files from GitHub API)
- /// - Times out after a specified amount of time in milliseconds, 100 000 milliseconds (=100 seconds) by default (https://stackoverflow.com/a/3052637)
- /// - Handles permanent redirects to the same host without clearing the Authorization header (needed to get files from renamed GitHub repositories via API)
- ///
- private sealed class RedirectingTimeoutWebClient : WebClient
- {
- ///
- /// Initialize our special web client
- ///
- /// Timeout for the request in milliseconds, defaulting to 100 000 (=100 seconds)
- /// A mime type sent with the "Accept" header
- public RedirectingTimeoutWebClient(int timeout = 100000, string mimeType = "")
- {
- this.timeout = timeout;
- this.mimeType = mimeType;
- }
-
- protected override WebRequest GetWebRequest(Uri address)
- {
- // Set user agent and MIME type for every request. including redirects
- Headers.Add("User-Agent", UserAgentString);
- if (!string.IsNullOrEmpty(mimeType))
- {
- log.InfoFormat("Setting MIME type {0}", mimeType);
- Headers.Add("Accept", mimeType);
- }
- if (permanentRedirects.TryGetValue(address, out Uri redirUri))
- {
- // Obey a previously received permanent redirect
- address = redirUri;
- }
- var request = base.GetWebRequest(address);
- if (request is HttpWebRequest hwr)
- {
- // GitHub API tokens cannot be passed via auto-redirect
- hwr.AllowAutoRedirect = false;
- hwr.Timeout = timeout;
- }
- return request;
- }
-
- protected override WebResponse GetWebResponse(WebRequest request)
- {
- if (request == null)
- return null;
- var response = base.GetWebResponse(request);
- if (response == null)
- return null;
-
- if (response is HttpWebResponse hwr)
- {
- int statusCode = (int)hwr.StatusCode;
- var location = hwr.Headers["Location"];
- if (statusCode >= 300 && statusCode <= 399 && location != null)
- {
- log.InfoFormat("Redirecting to {0}", location);
- hwr.Close();
- var redirUri = new Uri(request.RequestUri, location);
- if (Headers.AllKeys.Contains("Authorization")
- && request.RequestUri.Host != redirUri.Host)
- {
- log.InfoFormat("Host mismatch, purging token for redirect");
- Headers.Remove("Authorization");
- }
- // Moved or PermanentRedirect
- if (statusCode == 301 || statusCode == 308)
- {
- permanentRedirects.Add(request.RequestUri, redirUri);
- }
- return GetWebResponse(GetWebRequest(redirUri));
- }
- }
- return response;
- }
-
- private int timeout;
- private string mimeType;
- private static readonly Dictionary permanentRedirects = new Dictionary();
- }
-
- // HACK: The ancient WebClient doesn't support setting the request type to HEAD and WebRequest doesn't support
- // setting the User-Agent header.
- // Maybe one day we'll be able to use HttpClient (https://msdn.microsoft.com/en-us/library/system.net.http.httpclient%28v=vs.118%29.aspx)
- private sealed class RedirectWebClient : WebClient
- {
- public RedirectWebClient()
- {
- Headers.Add("User-Agent", UserAgentString);
- }
-
- protected override WebRequest GetWebRequest(Uri address)
- {
- var webRequest = (HttpWebRequest)base.GetWebRequest(address);
- webRequest.AllowAutoRedirect = false;
-
- webRequest.Method = "HEAD";
-
- return webRequest;
- }
- }
}
}
diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs
index 4ba4e33771..00c63ad92d 100644
--- a/Core/Net/NetAsyncDownloader.cs
+++ b/Core/Net/NetAsyncDownloader.cs
@@ -32,11 +32,14 @@ private class NetAsyncDownloaderDownloadPart
public Exception error;
public int lastProgressUpdateSize;
- public event DownloadProgressChangedEventHandler Progress;
+ ///
+ /// Percentage, bytes received, total bytes to receive
+ ///
+ public event Action Progress;
public event Action