/// <summary>
        /// Generates a diff of two mod manifests.
        /// </summary>
        /// <param name="newManifest">The new manifest.</param>
        /// <param name="oldManifest">The old manifest.</param>
        /// <returns>A list of <see cref="ModManifestDiff"/> containing change information.</returns>
        public static List <ModManifestDiff> Diff(List <ModManifestEntry> newManifest, List <ModManifestEntry> oldManifest)
        {
            // TODO: handle copies instead of moves to reduce download requirements (or cache downloads by hash?)

            var result = new List <ModManifestDiff>();

            List <ModManifestEntry> old = oldManifest != null && oldManifest.Count > 0
                                ? new List <ModManifestEntry>(oldManifest)
                                : new List <ModManifestEntry>();

            foreach (ModManifestEntry entry in newManifest)
            {
                // First, check for an exact match. File path/name, hash, size; everything.
                ModManifestEntry exact = old.FirstOrDefault(x => Equals(x, entry));

                if (exact != null)
                {
                    old.Remove(exact);
                    result.Add(new ModManifestDiff(ModManifestState.Unchanged, entry, null));
                    continue;
                }

                // There's no exact match, so let's search by checksum.
                List <ModManifestEntry> checksum = old.Where(x => x.Checksum.Equals(entry.Checksum, StringComparison.InvariantCultureIgnoreCase)).ToList();

                // If we've found matching checksums, we then need to check
                // the file path to see if it's been moved.
                if (checksum.Count > 0)
                {
                    old.Remove(checksum[0]);

                    if (checksum.All(x => x.FilePath != entry.FilePath))
                    {
                        old.Remove(old.FirstOrDefault(x => x.FilePath.Equals(entry.FilePath, StringComparison.InvariantCultureIgnoreCase)));
                        result.Add(new ModManifestDiff(ModManifestState.Moved, entry, checksum[0]));
                        continue;
                    }
                }

                // If we've made it here, there's no matching checksums, so let's search
                // for matching paths. If a path matches, the file has been modified.
                ModManifestEntry nameMatch = old.FirstOrDefault(x => x.FilePath.Equals(entry.FilePath, StringComparison.InvariantCultureIgnoreCase));

                if (nameMatch != null)
                {
                    old.Remove(nameMatch);
                    result.Add(new ModManifestDiff(ModManifestState.Changed, entry, nameMatch));
                    continue;
                }

                // In every other case, this file is newly added.
                result.Add(new ModManifestDiff(ModManifestState.Added, entry, null));
            }

            // All files that are still unique to the old manifest should be marked for removal.
            if (old.Count > 0)
            {
                result.AddRange(old.Select(x => new ModManifestDiff(ModManifestState.Removed, x, null)));
            }

            return(result);
        }
 public ModManifestDiff(ModManifestState state, ModManifestEntry current, ModManifestEntry last)
 {
     State   = state;
     Current = current;
     Last    = last;
 }
Exemplo n.º 3
0
		/// <summary>
		/// Downloads files required for updating according to <see cref="Type"/>.
		/// </summary>
		/// <param name="client"><see cref="WebClient"/> to be used for downloading.</param>
		/// <param name="updatePath">Path to store downloaded files.</param>
		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", StringComparison.OrdinalIgnoreCase))
					{
						var request = (HttpWebRequest)WebRequest.Create(uri);
						request.Method = "HEAD";
						var response = (HttpWebResponse)request.GetResponse();
						uri = response.ResponseUri;
						response.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 process = Process.Start(
						new ProcessStartInfo("7z.exe", $"x -aoa -o\"{dataDir}\" \"{filePath}\"")
						{
							UseShellExecute = false,
							CreateNoWindow  = true
						});

					if (process != null)
					{
						process.WaitForExit();
					}
					else
					{
						throw new NullReferenceException("Failed to create 7z process");
					}

					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<ModManifestEntry> newManifest = ModManifest.FromFile(newManPath);

					if (OnApplyingManifest(cancelArgs))
					{
						return;
					}

					List<ModManifestEntry> 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 (ModManifestEntry 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:
				{
					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.OrdinalIgnoreCase))
						{
							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)));
							}

							string hash = ModManifestGenerator.GetFileHash(filePath);
							if (!i.Current.Checksum.Equals(hash, StringComparison.OrdinalIgnoreCase))
							{
								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;

					// Handle all non-removal file operations (move, rename)
					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)
					{
						ModManifestEntry 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<ModManifestEntry> oldManifest = ModManifest.FromFile(oldManPath);
						List<ModManifestEntry> 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();
			}
		}