Esempio n. 1
0
        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();
            }
        }
Esempio n. 2
0
        // 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);
                        }
                    }
                }
            }
        }
Esempio n. 3
0
        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
            });
        }
Esempio n. 4
0
        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));
        }
Esempio n. 5
0
        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
            });
        }