/// <summary> /// Stores all of our files in the cache once done. /// Called by NetAsyncDownloader on completion. /// Called with all nulls on download cancellation. /// </summary> private void ModuleDownloadsComplete(NetModuleCache cache, Uri[] urls, string[] filenames, Exception[] errors) { if (filenames != null) { for (int i = 0; i < errors.Length; i++) { if (errors[i] == null) { // Cache the downloads that succeeded. try { cache.Store(modules[i], filenames[i], modules[i].StandardName()); } catch (FileNotFoundException e) { log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message); } } } // Finally, remove all our temp files. // We probably *could* have used Store's integrated move function above, but if we managed // to somehow get two URLs the same in our download set, that could cause right troubles! foreach (string tmpfile in filenames) { log.DebugFormat("Cleaning up {0}", tmpfile); File.Delete(tmpfile); } } }
private IEnumerable <ListViewItem> getRecSugRows( NetModuleCache cache, Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations, Dictionary <CkanModule, List <string> > suggestions, Dictionary <CkanModule, HashSet <string> > supporters ) { foreach (var kvp in recommendations) { yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value.Item2), RecommendationsGroup, kvp.Value.Item1)); } foreach (var kvp in suggestions) { yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value), SuggestionsGroup, false)); } foreach (var kvp in supporters .ToDictionary(kvp => kvp.Key, kvp => string.Join(", ", kvp.Value.OrderBy(s => s))) .OrderBy(kvp => kvp.Value) ) { yield return(getRecSugItem(cache, kvp.Key, string.Join(", ", kvp.Value), SupportedByGroup, false)); } }
/// <summary> /// Import a list of files into the download cache, with progress bar and /// interactive prompts for installation and deletion. /// </summary> /// <param name="files">Set of files to import</param> /// <param name="user">Object for user interaction</param> /// <param name="installMod">Function to call to mark a mod for installation</param> /// <param name="allowDelete">True to ask user whether to delete imported files, false to leave the files as is</param> public void ImportFiles(HashSet <FileInfo> files, IUser user, Action <string> installMod, bool allowDelete = true) { Registry registry = registry_manager.registry; HashSet <string> installable = new HashSet <string>(); List <FileInfo> deletable = new List <FileInfo>(); // Get the mapping of known hashes to modules Dictionary <string, List <CkanModule> > index = registry.GetSha1Index(); int i = 0; foreach (FileInfo f in files) { int percent = i * 100 / files.Count; user.RaiseProgress($"Importing {f.Name}... ({percent}%)", percent); // Calc SHA-1 sum string sha1 = NetModuleCache.GetFileHashSha1(f.FullName); // Find SHA-1 sum in registry (potentially multiple) if (index.ContainsKey(sha1)) { deletable.Add(f); List <CkanModule> matches = index[sha1]; foreach (CkanModule mod in matches) { if (mod.IsCompatibleKSP(ksp.VersionCriteria())) { installable.Add(mod.identifier); } if (Cache.IsMaybeCachedZip(mod)) { user.RaiseMessage("Already cached: {0}", f.Name); } else { user.RaiseMessage($"Importing {mod.identifier} {StripEpoch(mod.version)}..."); Cache.Store(mod, f.FullName); } } } else { user.RaiseMessage("Not found in index: {0}", f.Name); } ++i; } if (installable.Count > 0 && user.RaiseYesNoDialog($"Install {installable.Count} compatible imported mods in game instance {ksp.Name} ({ksp.GameDir()})?")) { // Install the imported mods foreach (string identifier in installable) { installMod(identifier); } } if (allowDelete && deletable.Count > 0 && user.RaiseYesNoDialog($"Import complete. Delete {deletable.Count} old files?")) { // Delete old files foreach (FileInfo f in deletable) { f.Delete(); } } }
public void LoadProviders(string requested, List <CkanModule> modules, NetModuleCache cache) { Util.Invoke(this, () => { ChooseProvidedModsLabel.Text = String.Format( Properties.Resources.MainInstallProvidedBy, requested ); ChooseProvidedModsListView.Items.Clear(); ChooseProvidedModsListView.Items.AddRange(modules .Select(module => new ListViewItem(new string[] { cache.IsMaybeCachedZip(module) ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)), module.@abstract }) { Tag = module, Checked = false }) .ToArray()); ChooseProvidedModsListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); ChooseProvidedModsContinueButton.Enabled = false; }); }
/// <summary> /// Downloads the given mod to the cache. Returns the filename it was saved to. /// </summary> public static string Download(CkanModule module, string filename, NetModuleCache cache) { log.Info("Downloading " + filename); string tmp_file = Net.Download(module.download); return(cache.Store(module, tmp_file, filename, true)); }
/// <summary> /// Returns a perfectly boring NetAsyncModulesDownloader. /// </summary> public NetAsyncModulesDownloader(IUser user, NetModuleCache cache) { modules = new List <CkanModule>(); downloader = new NetAsyncDownloader(user) { // Schedule us to process each module on completion. onOneCompleted = ModuleDownloadComplete }; this.cache = cache; }
/// <summary> /// Stores all of our files in the cache once done. /// Called by NetAsyncDownloader on completion. /// Called with all nulls on download cancellation. /// </summary> private void ModuleDownloadsComplete(NetModuleCache cache, Uri[] urls, string[] filenames, Exception[] errors) { if (urls != null) { // spawn up to 3 dialogs int errorDialogsLeft = 3; for (int i = 0; i < errors.Length; i++) { if (errors[i] != null) { if (errorDialogsLeft > 0) { User.RaiseError("Failed to download \"{0}\" - error: {1}", urls[i], errors[i].Message); errorDialogsLeft--; } } else { // Even if some of our downloads failed, we want to cache the // ones which succeeded. // This doesn't work :( // for some reason the tmp files get deleted before we get here and we get a nasty exception // not only that but then we try _to install_ the rest of the mods and then CKAN crashes // and the user's registry gets corrupted forever // commenting out until this is resolved // ~ nlight try { cache.Store(modules[i], filenames[i], modules[i].StandardName()); } catch (FileNotFoundException e) { log.WarnFormat("cache.Store(): FileNotFoundException: {0}", e.Message); } } } } if (filenames != null) { // Finally, remove all our temp files. // We probably *could* have used Store's integrated move function above, but if we managed // to somehow get two URLs the same in our download set, that could cause right troubles! foreach (string tmpfile in filenames) { log.DebugFormat("Cleaning up {0}", tmpfile); File.Delete(tmpfile); } } }
public void LoadRecommendations( NetModuleCache cache, Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations, Dictionary <CkanModule, List <string> > suggestions, Dictionary <CkanModule, HashSet <string> > supporters ) { RecommendedModsToggleCheckbox.Checked = true; RecommendedModsListView.Items.Clear(); RecommendedModsListView.Items.AddRange( getRecSugRows(cache, recommendations, suggestions, supporters).ToArray()); }
/// <summary> /// Returns a KSP object. /// Will initialise a CKAN instance in the KSP dir if it does not already exist, /// if the directory contains a valid KSP install. /// </summary> public KSP(string gameDir, string name, IUser user) { Name = name; User = user; // Make sure our path is absolute and has normalised slashes. this.gameDir = KSPPathUtils.NormalizePath(Path.GetFullPath(gameDir)); if (Valid) { SetupCkanDirectories(); LoadCompatibleVersions(); Cache = new NetModuleCache(DownloadCacheDir()); } }
private ListViewItem getRecSugItem(NetModuleCache cache, CkanModule module, string descrip, ListViewGroup group, bool check) { return(new ListViewItem(new string[] { cache.IsMaybeCachedZip(module) ? string.Format(Properties.Resources.MainChangesetCached, module.name, module.version) : string.Format(Properties.Resources.MainChangesetHostSize, module.name, module.version, module.download.Host ?? "", CkanModule.FmtSize(module.download_size)), descrip, module.@abstract }) { Tag = module, Checked = check, Group = group }); }
/// <summary> /// Returns the path to a cached copy of a module if it exists, or downloads /// and returns the downloaded copy otherwise. /// /// If no filename is provided, the module's standard name will be used. /// Chcecks provided cache first. /// </summary> public static string CachedOrDownload(CkanModule module, NetModuleCache cache, string filename = null) { if (filename == null) { filename = CkanModule.StandardName(module.identifier, module.version); } string full_path = cache.GetCachedZip(module); if (full_path == null) { return(Download(module, filename, cache)); } log.DebugFormat("Using {0} (cached)", filename); return(full_path); }
public void LoadRecommendations( IRegistryQuerier registry, KspVersionCriteria kspVersion, NetModuleCache cache, Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations, Dictionary <CkanModule, List <string> > suggestions, Dictionary <CkanModule, HashSet <string> > supporters ) { this.registry = registry; this.kspVersion = kspVersion; Util.Invoke(this, () => { RecommendedModsToggleCheckbox.Checked = true; RecommendedModsListView.Items.Clear(); RecommendedModsListView.Items.AddRange( getRecSugRows(cache, recommendations, suggestions, supporters).ToArray()); }); }
/// <summary> /// Returns a perfectly boring NetAsyncModulesDownloader. /// </summary> public NetAsyncModulesDownloader(IUser user, NetModuleCache cache) { modules = new List <CkanModule>(); downloader = new NetAsyncDownloader(user) { // Schedule us to process each module on completion. onOneCompleted = ModuleDownloadComplete }; downloader.Progress += (target, remaining, total) => { var mod = modules.FirstOrDefault(m => m.download == target.url); if (mod != null && Progress != null) { Progress(mod, remaining, total); } }; this.cache = cache; }
public void LoadRecommendations( IRegistryQuerier registry, HashSet <CkanModule> toInstall, HashSet <CkanModule> toUninstall, GameVersionCriteria GameVersion, NetModuleCache cache, Dictionary <CkanModule, Tuple <bool, List <string> > > recommendations, Dictionary <CkanModule, List <string> > suggestions, Dictionary <CkanModule, HashSet <string> > supporters ) { this.registry = registry; this.toInstall = toInstall; this.toUninstall = toUninstall; this.GameVersion = GameVersion; Util.Invoke(this, () => { RecommendedModsToggleCheckbox.Checked = true; RecommendedModsListView.Items.AddRange( getRecSugRows(cache, recommendations, suggestions, supporters).ToArray()); MarkConflicts(); }); }
/// <summary> /// <see cref="IDownloader.DownloadModules(NetFileCache, IEnumerable{CkanModule})"/> /// </summary> public void DownloadModules(NetModuleCache cache, IEnumerable <CkanModule> modules) { // Walk through all our modules, but only keep the first of each // one that has a unique download path. Dictionary <Uri, CkanModule> unique_downloads = modules .GroupBy(module => module.download) .ToDictionary(p => p.First().download, p => p.First()); this.modules.AddRange(unique_downloads.Values); // Schedule us to process our modules on completion. downloader.onCompleted = (_uris, paths, errors) => ModuleDownloadsComplete(cache, _uris, paths, errors); try { // Start the downloads! downloader.DownloadAndWait( unique_downloads.Select(item => new Net.DownloadTarget( item.Key, item.Value.InternetArchiveDownload, // Use a temp file name null, item.Value.download_size, // Send the MIME type to use for the Accept header // The GitHub API requires this to include application/octet-stream string.IsNullOrEmpty(item.Value.download_content_type) ? defaultMimeType : $"{item.Value.download_content_type};q=1.0,{defaultMimeType};q=0.9" )).ToList() ); } catch (DownloadErrorsKraken kraken) { // Associate the errors with the affected modules throw new ModuleDownloadErrorsKraken(this.modules, kraken); } }
/// <summary> /// <see cref="IDownloader.DownloadModules(NetFileCache, IEnumerable{CkanModule})"/> /// </summary> public void DownloadModules(NetModuleCache cache, IEnumerable <CkanModule> modules) { // Walk through all our modules, but only keep the first of each // one that has a unique download path. Dictionary <Uri, CkanModule> unique_downloads = modules .GroupBy(module => module.download) .ToDictionary(p => p.First().download, p => p.First()); this.modules.AddRange(unique_downloads.Values); // Schedule us to process our modules on completion. downloader.onCompleted = (_uris, paths, errors) => ModuleDownloadsComplete(cache, _uris, paths, errors); // retrieve the expected download size for each mod List <KeyValuePair <Uri, long> > downloads_with_size = unique_downloads .Select(item => new KeyValuePair <Uri, long>(item.Key, item.Value.download_size)) .ToList(); // Start the download! downloader.DownloadAndWait(downloads_with_size); }
/// <summary> /// Switch to using a download cache in a new location /// </summary> /// <param name="path">Location of folder for new cache</param> /// <param name="failureReason">Contains a human readable failure message if the setup failed</param> /// <returns> /// true if successful, false otherwise /// </returns> public bool TrySetupCache(string path, out string failureReason) { string origPath = Configuration.DownloadCacheDir; try { if (string.IsNullOrEmpty(path)) { Configuration.DownloadCacheDir = ""; Cache = new NetModuleCache(this, Configuration.DownloadCacheDir); } else { Cache = new NetModuleCache(this, path); Configuration.DownloadCacheDir = path; } failureReason = null; return(true); } catch (DirectoryNotFoundKraken) { failureReason = $"{path} does not exist"; return(false); } catch (PathErrorKraken ex) { failureReason = ex.Message; return(false); } catch (IOException ex) { // MoveFrom failed, possibly full disk, so undo the change Configuration.DownloadCacheDir = origPath; failureReason = ex.Message; return(false); } }
/// <summary> /// Download and update the local CKAN meta-info. /// Optionally takes a URL to the zipfile repo to download. /// Returns the number of unique modules updated. /// </summary> public static int UpdateAllRepositories(RegistryManager registry_manager, KSP ksp, NetModuleCache cache, IUser user) { SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories; List <CkanModule> allAvail = new List <CkanModule>(); foreach (KeyValuePair <string, Repository> repository in sortedRepositories) { log.InfoFormat("About to update {0}", repository.Value.name); SortedDictionary <string, int> downloadCounts; List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts); registry_manager.registry.SetDownloadCounts(downloadCounts); if (avail == null) { // Report failure if any repo fails, rather than losing half the list. // UpdateRegistry will have alerted the user to specific errors already. return(0); } else { log.InfoFormat("Updated {0}", repository.Value.name); // Merge all the lists allAvail.AddRange(avail); } } // Save allAvail to the registry if we found anything if (allAvail.Count > 0) { registry_manager.registry.SetAllAvailable(allAvail); // Save our changes. registry_manager.Save(enforce_consistency: false); ShowUserInconsistencies(registry_manager.registry, user); List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry); if (metadataChanges.Count > 0) { HandleModuleChanges(metadataChanges, user, ksp, cache); } // Return how many we got! return(registry_manager.registry.Available(ksp.VersionCriteria()).Count); } else { // Return failure return(0); } }
/// <summary> /// Resolve differences between installed and available metadata for given ModuleInstaller /// </summary> /// <param name="metadataChanges">List of modules that changed</param> /// <param name="user">Object for user interaction callbacks</param> /// <param name="ksp">Game instance</param> private static void HandleModuleChanges(List <CkanModule> metadataChanges, IUser user, KSP ksp, NetModuleCache cache) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < metadataChanges.Count; i++) { CkanModule module = metadataChanges[i]; sb.AppendLine(string.Format("- {0} {1}", module.identifier, module.version)); } if (user.RaiseYesNoDialog(string.Format(@"The following mods have had their metadata changed since last update: {0} You should reinstall them in order to preserve consistency with the repository. Do you wish to reinstall now?", sb))) { ModuleInstaller installer = ModuleInstaller.GetInstance(ksp, cache, new NullUser()); // New upstream metadata may break the consistency of already installed modules // e.g. if user installs modules A and B and then later up A is made to conflict with B // This is perfectly normal and shouldn't produce an error, therefore we skip enforcing // consistency. However, we will show the user any inconsistencies later on. // Use the identifiers so we use the overload that actually resolves relationships // Do each changed module one at a time so a failure of one doesn't cause all the others to fail foreach (string changedIdentifier in metadataChanges.Select(i => i.identifier)) { try { installer.Upgrade( new[] { changedIdentifier }, new NetAsyncModulesDownloader(new NullUser()), enforceConsistency: false ); } // Thrown when a dependency couldn't be satisfied catch (ModuleNotFoundKraken) { log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier); user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier); } // Thrown when a conflicts relationship is violated catch (InconsistentKraken) { log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier); user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier); } } } }
/// <summary> /// Download and update the local CKAN meta-info. /// Optionally takes a URL to the zipfile repo to download. /// </summary> public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, KSP ksp, NetModuleCache cache, IUser user) { SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories; if (sortedRepositories.Values.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) && repo.last_server_etag == Net.CurrentETag(repo.uri))) { user?.RaiseMessage("No changes since last update"); return(RepoUpdateResult.NoChanges); } List <CkanModule> allAvail = new List <CkanModule>(); foreach (KeyValuePair <string, Repository> repository in sortedRepositories) { log.InfoFormat("About to update {0}", repository.Value.name); SortedDictionary <string, int> downloadCounts; string newETag; List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts, out newETag); registry_manager.registry.SetDownloadCounts(downloadCounts); if (avail == null) { // Report failure if any repo fails, rather than losing half the list. // UpdateRegistry will have alerted the user to specific errors already. return(RepoUpdateResult.Failed); } else { log.InfoFormat("Updated {0}", repository.Value.name); // Merge all the lists allAvail.AddRange(avail); repository.Value.last_server_etag = newETag; } } // Save allAvail to the registry if we found anything if (allAvail.Count > 0) { registry_manager.registry.SetAllAvailable(allAvail); // Save our changes. registry_manager.Save(enforce_consistency: false); ShowUserInconsistencies(registry_manager.registry, user); List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry); if (metadataChanges.Count > 0) { HandleModuleChanges(metadataChanges, user, ksp, cache, registry_manager); } // Registry.CompatibleModules is slow, just return success, // caller can check it if it's really needed return(RepoUpdateResult.Updated); } else { // Return failure return(RepoUpdateResult.Failed); } }
/// <summary> /// Resolve differences between installed and available metadata for given ModuleInstaller /// </summary> /// <param name="metadataChanges">List of modules that changed</param> /// <param name="user">Object for user interaction callbacks</param> /// <param name="ksp">Game instance</param> /// <param name="cache">Cacne object for mod downloads</param> /// <param name="registry_manager">Manager that holds our game instances</param> private static void HandleModuleChanges(List <CkanModule> metadataChanges, IUser user, GameInstance ksp, NetModuleCache cache, RegistryManager registry_manager) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < metadataChanges.Count; i++) { CkanModule module = metadataChanges[i]; sb.AppendLine(string.Format("- {0} {1}", module.identifier, module.version)); } if (user.RaiseYesNoDialog(string.Format(@"The following mods have had their metadata changed since last update: {0} You should reinstall them in order to preserve consistency with the repository. Do you wish to reinstall now?", sb))) { throw new ReinstallModuleKraken(metadataChanges); } }
/// <summary> /// Returns a perfectly boring NetAsyncModulesDownloader. /// </summary> public NetAsyncModulesDownloader(IUser user, NetModuleCache cache) { modules = new List <CkanModule>(); downloader = new NetAsyncDownloader(user); this.cache = cache; }
/// <summary> /// Download and update the local CKAN meta-info. /// Optionally takes a URL to the zipfile repo to download. /// </summary> public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, GameInstance ksp, NetModuleCache cache, IUser user) { SortedDictionary <string, Repository> sortedRepositories = registry_manager.registry.Repositories; user.RaiseProgress("Checking for updates", 0); if (sortedRepositories.Values.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) && repo.last_server_etag == Net.CurrentETag(repo.uri))) { user.RaiseProgress("Already up to date", 100); user.RaiseMessage("No changes since last update"); return(RepoUpdateResult.NoChanges); } List <CkanModule> allAvail = new List <CkanModule>(); int index = 0; foreach (KeyValuePair <string, Repository> repository in sortedRepositories) { user.RaiseProgress($"Updating {repository.Value.name}", 10 + 80 * index / sortedRepositories.Count); SortedDictionary <string, int> downloadCounts; string newETag; List <CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts, out newETag); registry_manager.registry.SetDownloadCounts(downloadCounts); if (avail == null) { // Report failure if any repo fails, rather than losing half the list. // UpdateRegistry will have alerted the user to specific errors already. return(RepoUpdateResult.Failed); } else { // Merge all the lists allAvail.AddRange(avail); repository.Value.last_server_etag = newETag; user.RaiseMessage("Updated {0} ({1} modules)", repository.Value.name, avail.Count); } ++index; } // Save allAvail to the registry if we found anything if (allAvail.Count > 0) { user.RaiseProgress("Saving modules to registry", 90); using (var transaction = CkanTransaction.CreateTransactionScope()) { // Save our changes. registry_manager.registry.SetAllAvailable(allAvail); registry_manager.Save(enforce_consistency: false); transaction.Complete(); } ShowUserInconsistencies(registry_manager.registry, user); List <CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry); if (metadataChanges.Count > 0) { HandleModuleChanges(metadataChanges, user, ksp, cache, registry_manager); } // Registry.CompatibleModules is slow, just return success, // caller can check it if it's really needed user.RaiseProgress("Registry saved", 100); user.RaiseMessage("Repositories updated"); return(RepoUpdateResult.Updated); } else { // Return failure user.RaiseMessage("No modules found!"); return(RepoUpdateResult.Failed); } }