From 8fbd3544b45ce963f8efa75db3825dd1b50237db Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Fri, 9 Aug 2024 12:52:11 -0500 Subject: [PATCH] Refactor ZIP importing --- Core/Extensions/CryptoExtensions.cs | 8 +- Core/ModuleInstaller.cs | 183 +++++++++++++++++--------- Core/Net/NetAsyncModulesDownloader.cs | 2 +- Core/Net/NetFileCache.cs | 6 +- Core/Net/NetModuleCache.cs | 124 +++++++++-------- Core/Properties/Resources.fr-FR.resx | 4 +- Core/Properties/Resources.it-IT.resx | 2 +- Core/Properties/Resources.pl-PL.resx | 2 +- Core/Properties/Resources.resx | 5 +- Core/Properties/Resources.ru-RU.resx | 2 +- Core/Properties/Resources.zh-CN.resx | 2 +- GUI/Main/MainImport.cs | 60 +++++---- Netkan/Services/FileService.cs | 4 +- Tests/Core/Cache.cs | 6 +- Tests/Core/ModuleInstallerDirTest.cs | 2 +- Tests/Core/ModuleInstallerTests.cs | 18 +-- Tests/GUI/Model/ModList.cs | 2 +- 17 files changed, 259 insertions(+), 173 deletions(-) diff --git a/Core/Extensions/CryptoExtensions.cs b/Core/Extensions/CryptoExtensions.cs index 2dc2d88cdf..6c9dd44c1e 100644 --- a/Core/Extensions/CryptoExtensions.cs +++ b/Core/Extensions/CryptoExtensions.cs @@ -20,8 +20,10 @@ public static class CryptoExtensions /// Callback to notify as we traverse the input, called with percentages from 0 to 100 /// A cancellation token that can be used to abort the hash /// The requested hash of the input stream - public static byte[] ComputeHash(this HashAlgorithm hashAlgo, Stream stream, - IProgress progress, CancellationToken cancelToken = default) + public static byte[] ComputeHash(this HashAlgorithm hashAlgo, + Stream stream, + IProgress progress, + CancellationToken cancelToken = default) { const int bufSize = 1024 * 1024; var buffer = new byte[bufSize]; @@ -44,7 +46,7 @@ public static byte[] ComputeHash(this HashAlgorithm hashAlgo, Stream stream, } totalBytesRead += bytesRead; - progress.Report(100 * totalBytesRead / stream.Length); + progress.Report((int)(100 * totalBytesRead / stream.Length)); } return hashAlgo.Hash; } diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 62d783512c..6bfaee41f7 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -68,7 +68,7 @@ public static string Download(CkanModule module, string filename, NetModuleCache ServiceLocator.Container.Resolve().PreferredHosts)) .First()); - return cache.Store(module, tmp_file, new Progress(bytes => {}), filename, true); + return cache.Store(module, tmp_file, new Progress(percent => {}), filename, true); } /// @@ -1501,6 +1501,37 @@ public bool CanInstall(List toInstall, #endregion + private bool TryGetFileHashMatches(HashSet files, + Registry registry, + out Dictionary> matched, + out List notFound, + IProgress percentProgress) + { + matched = new Dictionary>(); + notFound = new List(); + var index = registry.GetDownloadHashesIndex(); + var progress = new ProgressScalePercentsByFileSizes(percentProgress, + files.Select(fi => fi.Length)); + foreach (var fi in files.Distinct()) + { + if (index.TryGetValue(Cache.GetFileHashSha256(fi.FullName, progress), + out List modules) + // The progress bar will jump back and "redo" the same span + // for non-matched files, but that's... OK? + || index.TryGetValue(Cache.GetFileHashSha1(fi.FullName, progress), + out modules)) + { + matched.Add(fi, modules); + } + else + { + notFound.Add(fi); + } + progress.NextFile(); + } + return matched.Count > 0; + } + /// /// Import a list of files into the download cache, with progress bar and /// interactive prompts for installation and deletion. @@ -1509,69 +1540,106 @@ public bool CanInstall(List toInstall, /// Object for user interaction /// Function to call to mark a mod for installation /// True to ask user whether to delete imported files, false to leave the files as is - public void ImportFiles(HashSet files, IUser user, Action installMod, Registry registry, bool allowDelete = true) + public bool ImportFiles(HashSet files, + IUser user, + Action installMod, + Registry registry, + bool allowDelete = true) { - HashSet installable = new HashSet(); - List deletable = new List(); - // Get the mapping of known hashes to modules - var index = registry.GetDownloadHashesIndex(); - int i = 0; - foreach (FileInfo f in files) + if (!TryGetFileHashMatches(files, registry, + out Dictionary> matched, + out List notFound, + new Progress(p => + user.RaiseProgress(Properties.Resources.ModuleInstallerImportScanningFiles, + p)))) { - int percent = i * 100 / files.Count; - user.RaiseProgress(string.Format(Properties.Resources.ModuleInstallerImporting, f.Name, percent), percent); - // Find SHA-256 or SHA-1 sum in registry (potentially multiple) - if (index.TryGetValue(Cache.GetFileHashSha256(f.FullName, new Progress(bytes => {})), - out List matches) - || index.TryGetValue(Cache.GetFileHashSha1(f.FullName, new Progress(bytes => {})), - out matches)) - { - deletable.Add(f); - foreach (var mod in matches) - { - if (mod.IsCompatible(instance.VersionCriteria())) - { - installable.Add(mod); - } - if (Cache.IsMaybeCachedZip(mod)) - { - user.RaiseMessage(Properties.Resources.ModuleInstallerImportAlreadyCached, f.Name); - } - else - { - user.RaiseMessage(Properties.Resources.ModuleInstallerImportingMod, - mod.identifier, StripEpoch(mod.version)); - Cache.Store(mod, f.FullName, new Progress(bytes => {})); - } - } - } - else + // We're not going to do anything, so let the user know they failed + user.RaiseError(Properties.Resources.ModuleInstallerImportNotFound, + string.Join(", ", notFound.Select(fi => fi.Name))); + return false; + } + + if (notFound.Count > 0) + { + // At least one was found, so just warn about the rest + user.RaiseMessage(" "); + user.RaiseMessage(Properties.Resources.ModuleInstallerImportNotFound, + string.Join(", ", notFound.Select(fi => fi.Name))); + } + var installable = matched.Values.SelectMany(modules => modules) + .Where(m => registry.IdentifierCompatible(m.identifier, + instance.VersionCriteria())) + .ToHashSet(); + + var deletable = matched.Keys.ToList(); + var delete = allowDelete + && deletable.Count > 0 + && user.RaiseYesNoDialog(string.Format(Properties.Resources.ModuleInstallerImportDeletePrompt, + deletable.Count)); + + // Store once per "primary" URL since each has its own URL hash + var cachedGroups = matched.SelectMany(kvp => kvp.Value.DistinctBy(m => m.download.First()) + .Select(m => (File: kvp.Key, + Module: m))) + .GroupBy(tuple => Cache.IsMaybeCachedZip(tuple.Module)) + .ToDictionary(grp => grp.Key, + grp => grp.ToArray()); + if (cachedGroups.TryGetValue(true, out (FileInfo File, CkanModule Module)[] alreadyStored)) + { + // Notify about files that are already cached + user.RaiseMessage(" "); + user.RaiseMessage(Properties.Resources.ModuleInstallerImportAlreadyCached, + string.Join(", ", alreadyStored.Select(tuple => $"{tuple.Module} ({tuple.File.Name})"))); + } + if (cachedGroups.TryGetValue(false, out (FileInfo File, CkanModule Module)[] toStore)) + { + // Store any new files + user.RaiseMessage(" "); + var description = ""; + var progress = new ProgressScalePercentsByFileSizes( + new Progress(p => + user.RaiseProgress(string.Format(Properties.Resources.ModuleInstallerImporting, + description), + p)), + toStore.Select(tuple => tuple.File.Length)); + foreach ((FileInfo fi, CkanModule module) in toStore) { - user.RaiseMessage(Properties.Resources.ModuleInstallerImportNotFound, f.Name); + + // Update the progress string + description = $"{module} ({fi.Name})"; + Cache.Store(module, fi.FullName, progress, + // Move if user said we could delete and we don't need to make any more copies + move: delete && toStore.Last(tuple => tuple.File == fi).Module == module, + // Skip revalidation because we had to check the hashes to get here! + validate: false); + progress.NextFile(); } - ++i; } - if (installable.Count > 0 && user.RaiseYesNoDialog(string.Format( - Properties.Resources.ModuleInstallerImportInstallPrompt, - installable.Count, instance.Name, instance.GameDir()))) + + // Here we have installable containing mods that can be installed, and the importable files have been stored in cache. + if (installable.Count > 0 + && user.RaiseYesNoDialog(string.Format(Properties.Resources.ModuleInstallerImportInstallPrompt, + installable.Count, + instance.Name, + instance.GameDir()))) { // Install the imported mods - foreach (CkanModule mod in installable) + foreach (var mod in installable) { installMod(mod); } } - if (allowDelete && deletable.Count > 0 && user.RaiseYesNoDialog(string.Format( - Properties.Resources.ModuleInstallerImportDeletePrompt, deletable.Count))) + if (delete) { - // Delete old files - foreach (FileInfo f in deletable) + // Delete old files that weren't already moved into cache + foreach (var f in deletable.Where(f => f.Exists)) { f.Delete(); } } EnforceCacheSizeLimit(registry); + return true; } private void EnforceCacheSizeLimit(Registry registry) @@ -1588,47 +1656,38 @@ private void EnforceCacheSizeLimit(Registry registry) /// Remove prepending v V. Version_ etc /// public static string StripV(string version) - { - Match match = Regex.Match(version, @"^(?\d\:)?[vV]+(ersion)?[_.]*(?\d.*)$"); - - return match.Success - ? match.Groups["num"].Value + match.Groups["ver"].Value - : version; - } + => Regex.Match(version, @"^(?\d\:)?[vV]+(ersion)?[_.]*(?\d.*)$") is Match match + && match.Success + ? match.Groups["num"].Value + match.Groups["ver"].Value + : version; /// /// Returns a version string shorn of any leading epoch as delimited by a single colon /// /// A version that might contain an epoch public static string StripEpoch(ModuleVersion version) - { - return StripEpoch(version.ToString()); - } + => StripEpoch(version.ToString()); /// /// Returns a version string shorn of any leading epoch as delimited by a single colon /// /// A version string that might contain an epoch public static string StripEpoch(string version) - { // If our version number starts with a string of digits, followed by // a colon, and then has no more colons, we're probably safe to assume // the first string of digits is an epoch - return epochMatch.IsMatch(version) + => epochMatch.IsMatch(version) ? epochReplace.Replace(version, @"$2") : version; - } /// /// As above, but includes the original in parentheses /// /// A version string that might contain an epoch public static string WithAndWithoutEpoch(string version) - { - return epochMatch.IsMatch(version) + => epochMatch.IsMatch(version) ? $"{epochReplace.Replace(version, @"$2")} ({version})" : version; - } private static readonly Regex epochMatch = new Regex(@"^[0-9][0-9]*:[^:]+$", RegexOptions.Compiled); private static readonly Regex epochReplace = new Regex(@"^([^:]+):([^:]+)$", RegexOptions.Compiled); diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index 29020be520..f7b57fbc6e 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -150,7 +150,7 @@ private void ModuleDownloadComplete(NetAsyncDownloader.DownloadTarget target, || m.InternetArchiveDownload == url); User.RaiseMessage(Properties.Resources.NetAsyncDownloaderValidating, module); cache.Store(module, filename, - new Progress(percent => StoreProgress?.Invoke(module, 100 - percent, 100)), + new Progress(percent => StoreProgress?.Invoke(module, 100 - percent, 100)), module.StandardName(), false, cancelTokenSrc.Token); diff --git a/Core/Net/NetFileCache.cs b/Core/Net/NetFileCache.cs index df152c8951..39f2d2e3a6 100644 --- a/Core/Net/NetFileCache.cs +++ b/Core/Net/NetFileCache.cs @@ -569,7 +569,7 @@ public static string CreateURLHash(Uri url) /// /// SHA1 hash, in all-caps hexadecimal format /// - public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) => GetFileHash(filePath, "sha1", sha1Cache, SHA1.Create, progress, cancelToken); /// @@ -580,7 +580,7 @@ public string GetFileHashSha1(string filePath, IProgress progress, Cancell /// /// SHA256 hash, in all-caps hexadecimal format /// - public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) => GetFileHash(filePath, "sha256", sha256Cache, SHA256.Create, progress, cancelToken); /// @@ -595,7 +595,7 @@ private string GetFileHash(string filePath, string hashSuffix, Dictionary cache, Func getHashAlgo, - IProgress progress, + IProgress progress, CancellationToken cancelToken) { string hashFile = $"{filePath}.{hashSuffix}"; diff --git a/Core/Net/NetModuleCache.cs b/Core/Net/NetModuleCache.cs index a69ce5dc84..049385673f 100644 --- a/Core/Net/NetModuleCache.cs +++ b/Core/Net/NetModuleCache.cs @@ -127,7 +127,7 @@ public string DescribeAvailability(CkanModule m) /// /// SHA1 hash, in all-caps hexadecimal format /// - public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha1(string filePath, IProgress progress, CancellationToken cancelToken = default) => cache.GetFileHashSha1(filePath, progress, cancelToken); /// @@ -138,7 +138,7 @@ public string GetFileHashSha1(string filePath, IProgress progress, Cancell /// /// SHA256 hash, in all-caps hexadecimal format /// - public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) + public string GetFileHashSha256(string filePath, IProgress progress, CancellationToken cancelToken = default) => cache.GetFileHashSha256(filePath, progress, cancelToken); /// @@ -153,73 +153,81 @@ public string GetFileHashSha256(string filePath, IProgress progress, Cance /// /// Name of the new file in the cache /// - public string Store(CkanModule module, string path, IProgress progress, string description = null, bool move = false, CancellationToken cancelToken = default) + public string Store(CkanModule module, + string path, + IProgress progress, + string description = null, + bool move = false, + CancellationToken cancelToken = default, + bool validate = true) { - // ZipValid takes a lot longer than the hash check, so scale them 70:30 if hashes are present - int zipValidPercent = module.download_hash == null ? 100 : 70; - - progress?.Report(0); - // Check file exists - FileInfo fi = new FileInfo(path); - if (!fi.Exists) + if (validate) { - throw new FileNotFoundKraken(path); - } + // ZipValid takes a lot longer than the hash check, so scale them 70:30 if hashes are present + int zipValidPercent = module.download_hash == null ? 100 : 70; - // Check file size - if (module.download_size > 0 && fi.Length != module.download_size) - { - throw new InvalidModuleFileKraken(module, path, string.Format( - Properties.Resources.NetModuleCacheBadLength, - module, path, fi.Length, module.download_size)); - } + progress?.Report(0); + // Check file exists + FileInfo fi = new FileInfo(path); + if (!fi.Exists) + { + throw new FileNotFoundKraken(path); + } - cancelToken.ThrowIfCancellationRequested(); + // Check file size + if (module.download_size > 0 && fi.Length != module.download_size) + { + throw new InvalidModuleFileKraken(module, path, string.Format( + Properties.Resources.NetModuleCacheBadLength, + module, path, fi.Length, module.download_size)); + } - // Check valid CRC - if (!ZipValid(path, out string invalidReason, new Progress(percent => - progress?.Report(percent * zipValidPercent / 100)))) - { - throw new InvalidModuleFileKraken(module, path, string.Format( - Properties.Resources.NetModuleCacheNotValidZIP, - module, path, invalidReason)); - } + cancelToken.ThrowIfCancellationRequested(); - cancelToken.ThrowIfCancellationRequested(); + // Check valid CRC + if (!ZipValid(path, out string invalidReason, new Progress(percent => + progress?.Report(percent * zipValidPercent / 100)))) + { + throw new InvalidModuleFileKraken(module, path, string.Format( + Properties.Resources.NetModuleCacheNotValidZIP, + module, path, invalidReason)); + } - // Some older metadata doesn't have hashes - if (module.download_hash != null) - { - int hashPercent = 100 - zipValidPercent; - // Only check one hash, sha256 if it's set, sha1 otherwise - if (!string.IsNullOrEmpty(module.download_hash.sha256)) + cancelToken.ThrowIfCancellationRequested(); + + // Some older metadata doesn't have hashes + if (module.download_hash != null) { - // Check SHA256 match - string sha256 = GetFileHashSha256(path, new Progress(percent => - progress?.Report(zipValidPercent + (percent * hashPercent / 100))), cancelToken); - if (sha256 != module.download_hash.sha256) + int hashPercent = 100 - zipValidPercent; + // Only check one hash, sha256 if it's set, sha1 otherwise + if (!string.IsNullOrEmpty(module.download_hash.sha256)) { - throw new InvalidModuleFileKraken(module, path, string.Format( - Properties.Resources.NetModuleCacheMismatchSHA256, - module, path, sha256, module.download_hash.sha256)); + // Check SHA256 match + string sha256 = GetFileHashSha256(path, new Progress(percent => + progress?.Report(zipValidPercent + (percent * hashPercent / 100))), cancelToken); + if (sha256 != module.download_hash.sha256) + { + throw new InvalidModuleFileKraken(module, path, string.Format( + Properties.Resources.NetModuleCacheMismatchSHA256, + module, path, sha256, module.download_hash.sha256)); + } } - } - else if (!string.IsNullOrEmpty(module.download_hash.sha1)) - { - // Check SHA1 match - string sha1 = GetFileHashSha1(path, new Progress(percent => - progress?.Report(zipValidPercent + (percent * hashPercent / 100))), cancelToken); - if (sha1 != module.download_hash.sha1) + else if (!string.IsNullOrEmpty(module.download_hash.sha1)) { - throw new InvalidModuleFileKraken(module, path, string.Format( - Properties.Resources.NetModuleCacheMismatchSHA1, - module, path, sha1, module.download_hash.sha1)); + // Check SHA1 match + string sha1 = GetFileHashSha1(path, new Progress(percent => + progress?.Report(zipValidPercent + (percent * hashPercent / 100))), cancelToken); + if (sha1 != module.download_hash.sha1) + { + throw new InvalidModuleFileKraken(module, path, string.Format( + Properties.Resources.NetModuleCacheMismatchSHA1, + module, path, sha1, module.download_hash.sha1)); + } } } - } - - cancelToken.ThrowIfCancellationRequested(); + cancelToken.ThrowIfCancellationRequested(); + } // If no exceptions, then everything is fine var success = cache.Store(module.download[0], path, description ?? module.StandardName(), move); // Make sure completion is signalled so progress bars go away @@ -237,7 +245,9 @@ public string Store(CkanModule module, string path, IProgress progress, st /// /// True if valid, false otherwise. See invalidReason param for explanation. /// - public static bool ZipValid(string filename, out string invalidReason, IProgress progress) + public static bool ZipValid(string filename, + out string invalidReason, + IProgress progress) { try { @@ -265,7 +275,7 @@ public static bool ZipValid(string filename, out string invalidReason, IProgress else if (st.Entry != null && progress != null) { // Report progress - var percent = 100 * st.Entry.ZipFileIndex / zip.Count; + var percent = (int)(100 * st.Entry.ZipFileIndex / zip.Count); if (percent > highestPercent) { progress.Report(percent); diff --git a/Core/Properties/Resources.fr-FR.resx b/Core/Properties/Resources.fr-FR.resx index b7c0076475..584017ab2d 100644 --- a/Core/Properties/Resources.fr-FR.resx +++ b/Core/Properties/Resources.fr-FR.resx @@ -515,7 +515,7 @@ Veuillez le retirer manuellement avant d'essayer de l'installer. Impossible de remplacer {0} car il n'est pas installé. Veuillez essayer d'installer {1} à la place. - Importation de {0}... ({1}%) + Importation de {0}... Déjà en cache : {0} @@ -593,7 +593,7 @@ Pensez à ajouter un jeton d'authentification pour augmenter la limite avant bri Fichier verrou avec un ID de processus actif à {0} -Un autre processus CKAN est probablement déjà en train d'accéder à ce dossier de jeu. Si vous êtes sûr que ce fichier verrou est périmé, vous pouvez supprimer ce fichier pour continuer. Mais si ce n'est pas le cas, les deux CKAN risque de rentrer en conflit et corrompre votre registre de mods et votre dossier de jeu. +Un autre processus CKAN est probablement déjà en train d'accéder à ce dossier de jeu. Si vous êtes sûr que ce fichier verrou est périmé, vous pouvez supprimer ce fichier pour continuer. Mais si ce n'est pas le cas, les deux CKAN risque de rentrer en conflit et corrompre votre registre de mods et votre dossier de jeu. Voulez-vous supprimez ce fichier verrou pour forcer l'accès ? diff --git a/Core/Properties/Resources.it-IT.resx b/Core/Properties/Resources.it-IT.resx index 6af7fed69c..d046eff568 100644 --- a/Core/Properties/Resources.it-IT.resx +++ b/Core/Properties/Resources.it-IT.resx @@ -504,7 +504,7 @@ Rimuovilo manualmente prima di provare a installarlo. Non è possibile sostituire {0} perché non è installato. Prova invece a installare {1}. - Importazione di {0}... ({1}%) + Importazione di {0}... Già in cache: {0} diff --git a/Core/Properties/Resources.pl-PL.resx b/Core/Properties/Resources.pl-PL.resx index 7021335d12..6adadec368 100644 --- a/Core/Properties/Resources.pl-PL.resx +++ b/Core/Properties/Resources.pl-PL.resx @@ -488,7 +488,7 @@ Proszę usunąć go ręcznie, zanim spróbujesz go zainstalować. Nie można zastąpić {0} ponieważ nie jest zainstalowany. Spróbuj zainstalować {1}. - Importowanie {0}... ({1}%) + Importowanie {0}... Już w folderze cache: {0} diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index 86d8a5b486..244d9eb245 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -255,12 +255,13 @@ Overwrite? Can't replace {0} as it was not installed by CKAN. Please remove manually before trying to install it. Can't replace {0} as it is not installed. Please attempt to install {1} instead. - Importing {0}... ({1}%) + Scanning files... + Importing {0}... Already cached: {0} Importing {0} {1}... Not found in index: {0} Install {0} compatible imported mods in game instance {1} ({2})? - Import complete. Delete {0} old files? + Scanning complete. Delete {0} old files after import? Dependency on {0} version {1} not satisfied Module not found: {0} {1} {0} dependency on {1} version {2} not satisfied diff --git a/Core/Properties/Resources.ru-RU.resx b/Core/Properties/Resources.ru-RU.resx index 268c85b6a3..6bb669f2ca 100644 --- a/Core/Properties/Resources.ru-RU.resx +++ b/Core/Properties/Resources.ru-RU.resx @@ -245,7 +245,7 @@ Невозможно заменить {0}, так как он не был установлен CKAN. Удалите его вручную и попробуйте снова. Невозможно заменить {0}, так как он не установлен. Сначала установите {1}. - Импорт {0}... ({1}%) + Импорт {0}... Уже кэшировано: {0} Импорт {0} {1}... Не найдено в указателе: {0} diff --git a/Core/Properties/Resources.zh-CN.resx b/Core/Properties/Resources.zh-CN.resx index 2176039dc7..e70a221242 100644 --- a/Core/Properties/Resources.zh-CN.resx +++ b/Core/Properties/Resources.zh-CN.resx @@ -506,7 +506,7 @@ 无法替换 {0} 因为它还未安装。请尝试安装 {1}。 - 导入 {0}... ({1}%) + 导入 {0}... 已缓存: {0} diff --git a/GUI/Main/MainImport.cs b/GUI/Main/MainImport.cs index 04c515e7a3..afd5a9e8b0 100644 --- a/GUI/Main/MainImport.cs +++ b/GUI/Main/MainImport.cs @@ -19,7 +19,7 @@ private void importDownloadsToolStripMenuItem_Click(object sender, EventArgs e) private void ImportModules() { // Prompt the user to select one or more ZIP files - OpenFileDialog dlg = new OpenFileDialog() + var dlg = new OpenFileDialog() { Title = Properties.Resources.MainImportTitle, AddExtension = true, @@ -37,31 +37,45 @@ private void ImportModules() tabController.RenameTab("WaitTabPage", Properties.Resources.MainImportWaitTitle); ShowWaitDialog(); DisableMainWindow(); - - try - { - new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser).ImportFiles( - GetFiles(dlg.FileNames), - currentUser, - (CkanModule mod) => + Wait.StartWaiting( + (sender, e) => + { + e.Result = new ModuleInstaller(CurrentInstance, Manager.Cache, currentUser).ImportFiles( + GetFiles(dlg.FileNames), + currentUser, + (CkanModule mod) => + { + if (ManageMods.mainModList + .full_list_of_mod_rows + .TryGetValue(mod.identifier, + out DataGridViewRow row) + && row.Tag is GUIMod gmod) + { + gmod.SelectedMod = mod; + } + }, + RegistryManager.Instance(CurrentInstance, repoData).registry); + }, + (sender, e) => + { + if (e.Error == null && e.Result is bool result && result) + { + // Put GUI back the way we found it + HideWaitDialog(); + } + else { - if (ManageMods.mainModList - .full_list_of_mod_rows - .TryGetValue(mod.identifier, - out DataGridViewRow row) - && row.Tag is GUIMod gmod) + if (e.Error is Exception exc) { - gmod.SelectedMod = mod; + log.Error(exc.Message, exc); + currentUser.RaiseMessage(exc.Message); } - }, - RegistryManager.Instance(CurrentInstance, repoData).registry); - } - finally - { - // Put GUI back the way we found it - EnableMainWindow(); - HideWaitDialog(); - } + Wait.Finish(); + } + EnableMainWindow(); + }, + false, + null); } } diff --git a/Netkan/Services/FileService.cs b/Netkan/Services/FileService.cs index 94df591026..9a109b5044 100644 --- a/Netkan/Services/FileService.cs +++ b/Netkan/Services/FileService.cs @@ -16,12 +16,12 @@ public long GetSizeBytes(string filePath) public string GetFileHashSha1(string filePath) // Use shared implementation from Core. // Also needs to be an instance method so it can be Moq'd for testing. - => cache.GetFileHashSha1(filePath, new Progress(bytes => {})); + => cache.GetFileHashSha1(filePath, new Progress(percent => {})); public string GetFileHashSha256(string filePath) // Use shared implementation from Core. // Also needs to be an instance method so it can be Moq'd for testing. - => cache.GetFileHashSha256(filePath, new Progress(bytes => {})); + => cache.GetFileHashSha256(filePath, new Progress(percent => {})); public string GetMimetype(string filePath) { diff --git a/Tests/Core/Cache.cs b/Tests/Core/Cache.cs index 51e964290a..2ac98103ba 100644 --- a/Tests/Core/Cache.cs +++ b/Tests/Core/Cache.cs @@ -115,14 +115,14 @@ public void StoreInvalid() Assert.Throws(() => module_cache.Store( TestData.DogeCoinFlag_101_LZMA_module, - "/DoesNotExist.zip", new Progress(bytes => {}))); + "/DoesNotExist.zip", new Progress(percent => {}))); // Try to store the LZMA-format DogeCoin zip into a NetModuleCache // and expect an InvalidModuleFileKraken Assert.Throws(() => module_cache.Store( TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZipLZMA, new Progress(bytes => {}))); + TestData.DogeCoinFlagZipLZMA, new Progress(percent => {}))); // Try to store the normal DogeCoin zip into a NetModuleCache // using the WRONG metadata (file size and hashes) @@ -130,7 +130,7 @@ public void StoreInvalid() Assert.Throws(() => module_cache.Store( TestData.DogeCoinFlag_101_LZMA_module, - TestData.DogeCoinFlagZip(), new Progress(bytes => {}))); + TestData.DogeCoinFlagZip(), new Progress(percent => {}))); } [Test] diff --git a/Tests/Core/ModuleInstallerDirTest.cs b/Tests/Core/ModuleInstallerDirTest.cs index 9f93097f1e..24cff47bc2 100644 --- a/Tests/Core/ModuleInstallerDirTest.cs +++ b/Tests/Core/ModuleInstallerDirTest.cs @@ -58,7 +58,7 @@ public void SetUp() _gameDir = _instance.KSP.GameDir(); _gameDataDir = _instance.KSP.game.PrimaryModDirectory(_instance.KSP); var testModFile = TestData.DogeCoinFlagZip(); - _manager.Cache.Store(_testModule, testModFile, new Progress(bytes => {})); + _manager.Cache.Store(_testModule, testModFile, new Progress(percent => {})); HashSet possibleConfigOnlyDirs = null; _installer.InstallList( new List() { _testModule }, diff --git a/Tests/Core/ModuleInstallerTests.cs b/Tests/Core/ModuleInstallerTests.cs index ec030a8ce2..2d691e58a0 100644 --- a/Tests/Core/ModuleInstallerTests.cs +++ b/Tests/Core/ModuleInstallerTests.cs @@ -413,7 +413,7 @@ public void CanInstallMod() string cache_path = manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(bytes => {})); + new Progress(percent => {})); Assert.IsTrue(manager.Cache.IsCached(TestData.DogeCoinFlag_101_module())); Assert.IsTrue(File.Exists(cache_path)); @@ -462,7 +462,7 @@ public void CanUninstallMod() registry.RepositoriesAdd(repo.repo); manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(bytes => {})); + new Progress(percent => {})); var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -510,7 +510,7 @@ public void UninstallEmptyDirs() registry.RepositoriesAdd(repo.repo); manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(bytes => {})); + new Progress(percent => {})); var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -526,7 +526,7 @@ public void UninstallEmptyDirs() // Install the plugin test mod. manager.Cache.Store(TestData.DogeCoinPlugin_module(), TestData.DogeCoinPluginZip(), - new Progress(bytes => {})); + new Progress(percent => {})); modules.Add(TestData.DogeCoinPlugin_module()); @@ -719,7 +719,7 @@ public void ModuleManagerInstancesAreDecoupled() // Copy the zip file to the cache directory. manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), - new Progress(bytes => {})); + new Progress(percent => {})); // Attempt to install it. var modules = new List { TestData.DogeCoinFlag_101_module() }; @@ -875,7 +875,7 @@ public void Replace_WithCompatibleModule_Succeeds() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); - manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); + manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(percent => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 12))); installer.Replace(Enumerable.Repeat(replacement, 1), @@ -941,7 +941,7 @@ public void Replace_WithIncompatibleModule_Fails() // Act registry.RegisterModule(replaced, new List(), inst.KSP, false); - manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(bytes => {})); + manager.Cache.Store(replaced, TestData.DogeCoinFlagZip(), new Progress(percent => {})); var replacement = querier.GetReplacement(replaced.identifier, new GameVersionCriteria(new GameVersion(1, 11))); @@ -1094,7 +1094,7 @@ public void Upgrade_WithAutoInst_RemovesAutoRemovable(string[] regularMods, var module = CkanModule.FromJson(m); manager.Cache.Store(module, TestData.DogeCoinFlagZip(), - new Progress(bytes => {})); + new Progress(percent => {})); if (!querier.IsInstalled(module.identifier, false)) { registry.RegisterModule(module, @@ -1166,7 +1166,7 @@ private void installTestPlugin(string unmanaged, string moduleJson, string zipPa File.WriteAllText(inst.KSP.ToAbsoluteGameDir(unmanaged), "Not really a DLL, are we?"); regMgr.ScanUnmanagedFiles(); - manager.Cache.Store(module, zipPath, new Progress(bytes => {})); + manager.Cache.Store(module, zipPath, new Progress(percent => {})); // Act HashSet possibleConfigOnlyDirs = null; diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index ade5510b9c..82939afdca 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -160,7 +160,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() // Act // Install module and set it as pre-installed - manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(bytes => {})); + manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(percent => {})); registry.RegisterModule(anyVersionModule, new List(), instance.KSP, false); HashSet possibleConfigOnlyDirs = null;