public void Download(WebClient client, string updatePath) { var cancelArgs = new CancelEventArgs(false); DownloadProgressEventArgs downloadArgs = null; int fileDownloading = 0; void DownloadComplete(object sender, AsyncCompletedEventArgs args) { lock (args.UserState) { Monitor.Pulse(args.UserState); } } void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs args) { downloadArgs = new DownloadProgressEventArgs(args, fileDownloading, FilesToDownload); if (OnDownloadProgress(downloadArgs)) { ((WebClient)sender).CancelAsync(); } } switch (Type) { case ModDownloadType.Archive: { var uri = new Uri(Url); if (!uri.Host.EndsWith("github.com")) { HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri); req.Method = "HEAD"; HttpWebResponse res = (HttpWebResponse)req.GetResponse(); uri = res.ResponseUri; res.Close(); } string filePath = Path.Combine(updatePath, uri.Segments.Last()); var info = new FileInfo(filePath); if (info.Exists && info.Length == Size) { if (OnDownloadCompleted(cancelArgs)) { return; } } else { if (OnDownloadStarted(cancelArgs)) { return; } client.DownloadFileCompleted += DownloadComplete; client.DownloadProgressChanged += DownloadProgressChanged; ++fileDownloading; var sync = new object(); lock (sync) { client.DownloadFileAsync(uri, filePath, sync); Monitor.Wait(sync); } client.DownloadProgressChanged -= DownloadProgressChanged; client.DownloadFileCompleted -= DownloadComplete; if (cancelArgs.Cancel || downloadArgs?.Cancel == true) { return; } if (OnDownloadCompleted(cancelArgs)) { return; } } string dataDir = Path.Combine(updatePath, Path.GetFileNameWithoutExtension(filePath)); if (!Directory.Exists(dataDir)) { Directory.CreateDirectory(dataDir); } if (OnExtracting(cancelArgs)) { return; } Process.Start(new ProcessStartInfo("7za.exe", $"x -aoa -o\"{dataDir}\" \"{filePath}\"") { UseShellExecute = false, CreateNoWindow = true }).WaitForExit(); string workDir = Path.GetDirectoryName(ModInfo.GetModFiles(new DirectoryInfo(dataDir)).FirstOrDefault()); if (string.IsNullOrEmpty(workDir)) { throw new DirectoryNotFoundException("Unable to locate mod.ini in " + dataDir); } string newManPath = Path.Combine(workDir, "mod.manifest"); string oldManPath = Path.Combine(Folder, "mod.manifest"); if (OnParsingManifest(cancelArgs)) { return; } if (!File.Exists(newManPath) || !File.Exists(oldManPath)) { CopyDirectory(new DirectoryInfo(workDir), Directory.CreateDirectory(Folder)); Directory.Delete(dataDir, true); if (File.Exists(filePath)) { File.Delete(filePath); } return; } if (OnParsingManifest(cancelArgs)) { return; } List <ModManifest> newManifest = ModManifest.FromFile(newManPath); if (OnApplyingManifest(cancelArgs)) { return; } List <ModManifest> oldManifest = ModManifest.FromFile(oldManPath); List <string> oldFiles = oldManifest.Except(newManifest) .Select(x => Path.Combine(Folder, x.FilePath)) .ToList(); foreach (string file in oldFiles) { if (File.Exists(file)) { File.Delete(file); } } RemoveEmptyDirectories(oldManifest, newManifest); foreach (ModManifest file in newManifest) { string dir = Path.GetDirectoryName(file.FilePath); if (!string.IsNullOrEmpty(dir)) { string newDir = Path.Combine(Folder, dir); if (!Directory.Exists(newDir)) { Directory.CreateDirectory(newDir); } } string dest = Path.Combine(Folder, file.FilePath); if (File.Exists(dest)) { File.Delete(dest); } File.Move(Path.Combine(workDir, file.FilePath), dest); } File.Copy(newManPath, oldManPath, true); Directory.Delete(dataDir, true); File.WriteAllText(Path.Combine(Folder, "mod.version"), Updated); if (File.Exists(filePath)) { File.Delete(filePath); } break; } case ModDownloadType.Modular: { // First let's download all the new stuff. List <ModManifestDiff> newEntries = ChangedFiles .Where(x => x.State == ModManifestState.Added || x.State == ModManifestState.Changed) .ToList(); var uri = new Uri(Url); string tempDir = Path.Combine(updatePath, uri.Segments.Last()); if (!Directory.Exists(tempDir)) { Directory.CreateDirectory(tempDir); } var sync = new object(); foreach (ModManifestDiff i in newEntries) { string filePath = Path.Combine(tempDir, i.Current.FilePath); string dir = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } if (OnDownloadStarted(cancelArgs)) { return; } var info = new FileInfo(filePath); ++fileDownloading; if (!info.Exists || info.Length != i.Current.FileSize || !i.Current.Checksum.Equals(ModManifestGenerator.GetFileHash(filePath), StringComparison.InvariantCultureIgnoreCase)) { client.DownloadFileCompleted += DownloadComplete; client.DownloadProgressChanged += DownloadProgressChanged; lock (sync) { client.DownloadFileAsync(new Uri(uri, i.Current.FilePath), filePath, sync); Monitor.Wait(sync); } client.DownloadProgressChanged -= DownloadProgressChanged; client.DownloadFileCompleted -= DownloadComplete; info.Refresh(); if (info.Length != i.Current.FileSize) { throw new Exception(string.Format("Size of downloaded file \"{0}\" ({1}) differs from manifest ({2}).", i.Current.FilePath, SizeSuffix.GetSizeSuffix(info.Length), SizeSuffix.GetSizeSuffix(i.Current.FileSize))); } var hash = ModManifestGenerator.GetFileHash(filePath); if (!i.Current.Checksum.Equals(hash, StringComparison.InvariantCultureIgnoreCase)) { throw new Exception(string.Format("Checksum of downloaded file \"{0}\" ({1}) differs from manifest ({2}).", i.Current.FilePath, hash, i.Current.Checksum)); } } if (cancelArgs.Cancel || downloadArgs?.Cancel == true) { return; } if (OnDownloadCompleted(cancelArgs)) { return; } } client.DownloadFileCompleted += DownloadComplete; lock (sync) { client.DownloadFileAsync(new Uri(uri, "mod.manifest"), Path.Combine(tempDir, "mod.manifest"), sync); Monitor.Wait(sync); } client.DownloadFileCompleted -= DownloadComplete; // Now handle all file operations except where removals are concerned. List <ModManifestDiff> movedEntries = ChangedFiles.Except(newEntries) .Where(x => x.State == ModManifestState.Moved) .ToList(); if (OnApplyingManifest(cancelArgs)) { return; } // Handle existing entries marked as moved. foreach (ModManifestDiff i in movedEntries) { ModManifest old = i.Last; // This would be considered an error... if (old == null) { continue; } string oldPath = Path.Combine(Folder, old.FilePath); string newPath = Path.Combine(tempDir, i.Current.FilePath); string dir = Path.GetDirectoryName(newPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } File.Copy(oldPath, newPath, true); } // Now move the stuff from the temporary folder over to the working directory. foreach (ModManifestDiff i in newEntries.Concat(movedEntries)) { string tempPath = Path.Combine(tempDir, i.Current.FilePath); string workPath = Path.Combine(Folder, i.Current.FilePath); string dir = Path.GetDirectoryName(workPath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } File.Copy(tempPath, workPath, true); } // Once that has succeeded we can safely delete files that have been marked for removal. List <ModManifestDiff> removedEntries = ChangedFiles .Where(x => x.State == ModManifestState.Removed) .ToList(); foreach (string path in removedEntries.Select(i => Path.Combine(Folder, i.Current.FilePath)).Where(File.Exists)) { File.Delete(path); } // Same for files that have been moved. foreach (string path in movedEntries .Where(x => newEntries.All(y => y.Current.FilePath != x.Last.FilePath)) .Select(i => Path.Combine(Folder, i.Last.FilePath)).Where(File.Exists)) { File.Delete(path); } string oldManPath = Path.Combine(Folder, "mod.manifest"); string newManPath = Path.Combine(tempDir, "mod.manifest"); if (File.Exists(oldManPath)) { List <ModManifest> oldManifest = ModManifest.FromFile(oldManPath); List <ModManifest> newManifest = ModManifest.FromFile(newManPath); // Remove directories that are now empty. RemoveEmptyDirectories(oldManifest, newManifest); } // And last but not least, copy over the new manifest. File.Copy(newManPath, oldManPath, true); break; } default: throw new ArgumentOutOfRangeException(); } }
// TODO: cancel /// <summary> /// Get mod update metadata for the provided mods. /// </summary> /// <param name="updatableMods">Key-value pairs of mods to be checked, where the key is the mod path and the value is the mod metadata.</param> /// <param name="updates">Output list of mods with available updates.</param> /// <param name="errors">Output list of errors encountered during the update process.</param> public void GetModUpdates(List <KeyValuePair <string, ModInfo> > updatableMods, out List <ModDownload> updates, out List <string> errors, CancellationToken cancellationToken) { updates = new List <ModDownload>(); errors = new List <string>(); if (updatableMods == null || updatableMods.Count == 0) { return; } using (var client = new UpdaterWebClient()) { foreach (KeyValuePair <string, ModInfo> info in updatableMods) { ModInfo mod = info.Value; if (!string.IsNullOrEmpty(mod.GitHubRepo)) { if (string.IsNullOrEmpty(mod.GitHubAsset)) { errors.Add($"[{mod.Name}] GitHubRepo specified, but GitHubAsset is missing."); continue; } ModDownload d = GetGitHubReleases(mod, info.Key, client, errors); if (d != null) { updates.Add(d); } } else if (!string.IsNullOrEmpty(mod.GameBananaItemType) && mod.GameBananaItemId.HasValue) { ModDownload d = GetGameBananaReleases(mod, info.Key, errors); if (d != null) { updates.Add(d); } } else if (!string.IsNullOrEmpty(mod.UpdateUrl)) { List <ModManifestEntry> localManifest = null; string manPath = Path.Combine("mods", info.Key, "mod.manifest"); if (!ForceUpdate && File.Exists(manPath)) { try { localManifest = ModManifest.FromFile(manPath); } catch (Exception ex) { errors.Add($"[{mod.Name}] Error parsing local manifest: {ex.Message}"); continue; } } ModDownload d = CheckModularVersion(mod, info.Key, localManifest, client, errors); if (d != null) { updates.Add(d); } } } } }
public ModDownload GetGitHubReleases(ModInfo mod, string folder, UpdaterWebClient client, List <string> errors) { List <GitHubRelease> releases; string url = "https://api.github.com/repos/" + mod.GitHubRepo + "/releases"; if (!gitHubCache.ContainsKey(url)) { try { string text = client.DownloadString(url); releases = JsonConvert.DeserializeObject <List <GitHubRelease> >(text) .Where(x => !x.Draft && !x.PreRelease) .ToList(); if (releases.Count > 0) { gitHubCache[url] = releases; } } catch (Exception ex) { errors.Add($"[{mod.Name}] Error checking for updates at {url}: {ex.Message}"); return(null); } } else { releases = gitHubCache[url]; } if (releases == null || releases.Count == 0) { // No releases available. return(null); } string versionPath = Path.Combine("mods", folder, "mod.version"); DateTime?localVersion = null; if (File.Exists(versionPath)) { localVersion = DateTime.Parse(File.ReadAllText(versionPath).Trim()); } else { var info = new FileInfo(Path.Combine("mods", folder, "mod.manifest")); if (info.Exists) { localVersion = info.LastWriteTimeUtc; } } GitHubRelease latestRelease = null; GitHubAsset latestAsset = null; foreach (GitHubRelease release in releases) { GitHubAsset asset = release.Assets .FirstOrDefault(x => x.Name.Equals(mod.GitHubAsset, StringComparison.OrdinalIgnoreCase)); if (asset == null) { continue; } latestRelease = release; if (!ForceUpdate && localVersion.HasValue) { DateTime uploaded = DateTime.Parse(asset.Uploaded); if (localVersion >= uploaded) { // No updates available. break; } } latestAsset = asset; break; } if (latestRelease == null) { errors.Add($"[{mod.Name}] No releases with matching asset \"{mod.GitHubAsset}\" could be found in {releases.Count} release(s)."); return(null); } if (latestAsset == null) { return(null); } string body = Regex.Replace(latestRelease.Body, "(?<!\r)\n", "\r\n"); return(new ModDownload(mod, Path.Combine("mods", folder), latestAsset.DownloadUrl, body, latestAsset.Size) { HomePage = "https://github.com/" + mod.GitHubRepo, Name = latestRelease.Name, Version = latestRelease.TagName, Published = latestRelease.Published, Updated = latestAsset.Uploaded, ReleaseUrl = latestRelease.HtmlUrl }); }
public ModDownload CheckModularVersion(ModInfo mod, string folder, List <ModManifestEntry> localManifest, UpdaterWebClient client, List <string> errors) { if (!mod.UpdateUrl.StartsWith("http://", StringComparison.InvariantCulture) && !mod.UpdateUrl.StartsWith("https://", StringComparison.InvariantCulture)) { mod.UpdateUrl = "http://" + mod.UpdateUrl; } if (!mod.UpdateUrl.EndsWith("/", StringComparison.InvariantCulture)) { mod.UpdateUrl += "/"; } var url = new Uri(mod.UpdateUrl); url = new Uri(url, "mod.ini"); ModInfo remoteInfo; try { Dictionary <string, Dictionary <string, string> > dict = IniFile.IniFile.Load(client.OpenRead(url)); remoteInfo = IniSerializer.Deserialize <ModInfo>(dict); } catch (Exception ex) { errors.Add($"[{mod.Name}] Error pulling mod.ini from \"{mod.UpdateUrl}\": {ex.Message}"); return(null); } if (!ForceUpdate && remoteInfo.Version == mod.Version) { return(null); } string manString; try { manString = client.DownloadString(new Uri(new Uri(mod.UpdateUrl), "mod.manifest")); } catch (Exception ex) { errors.Add($"[{mod.Name}] Error pulling mod.manifest from \"{mod.UpdateUrl}\": {ex.Message}"); return(null); } List <ModManifestEntry> remoteManifest; try { remoteManifest = ModManifest.FromString(manString); } catch (Exception ex) { errors.Add($"[{mod.Name}] Error parsing remote manifest from \"{mod.UpdateUrl}\": {ex.Message}"); return(null); } List <ModManifestDiff> diff = ModManifestGenerator.Diff(remoteManifest, localManifest); if (diff.Count < 1 || diff.All(x => x.State == ModManifestState.Unchanged)) { return(null); } string changes; if (!string.IsNullOrEmpty(mod.ChangelogUrl)) { try { changes = client.DownloadString(new Uri(mod.ChangelogUrl)); } catch (Exception ex) { changes = ex.Message; } } else { try { changes = client.DownloadString(new Uri(new Uri(mod.UpdateUrl), "changelog.txt")); } catch { // ignored changes = string.Empty; } } if (!string.IsNullOrEmpty(changes)) { changes = Regex.Replace(changes, "(?<!\r)\n", "\r\n"); } return(new ModDownload(mod, Path.Combine("mods", folder), mod.UpdateUrl, changes, diff)); }
public ModDownload GetGameBananaReleases(ModInfo mod, string folder, List <string> errors) { GameBananaItem gbi; try { gbi = GameBananaItem.Load(mod.GameBananaItemType, mod.GameBananaItemId.Value); } catch (Exception ex) { errors.Add($"[{mod.Name}] Error checking for updates: {ex.Message}"); return(null); } if (!gbi.HasUpdates) { // No releases available. return(null); } string versionPath = Path.Combine("mods", folder, "mod.version"); DateTime?localVersion = null; if (File.Exists(versionPath)) { localVersion = DateTime.Parse(File.ReadAllText(versionPath).Trim()); } else { var info = new FileInfo(Path.Combine("mods", folder, "mod.manifest")); if (info.Exists) { localVersion = info.LastWriteTimeUtc; } } GameBananaItemUpdate latestUpdate = gbi.Updates[0]; if (!ForceUpdate && localVersion.HasValue) { if (localVersion >= latestUpdate.DateAdded) { // No updates available. return(null); } } string body = string.Join(Environment.NewLine, latestUpdate.Changes.Select(a => a.Category + ": " + a.Text)) + Environment.NewLine + latestUpdate.Text; GameBananaItemFile dl = gbi.Files.First().Value; return(new ModDownload(mod, Path.Combine("mods", folder), dl.DownloadUrl, body, dl.Filesize) { HomePage = gbi.ProfileUrl, Name = latestUpdate.Title, Version = latestUpdate.Title, Published = latestUpdate.DateAdded.ToString(CultureInfo.CurrentCulture), Updated = latestUpdate.DateAdded.ToString(CultureInfo.CurrentCulture), ReleaseUrl = gbi.ProfileUrl }); }