public static bool UpdateMod(ModUpdateInfo updateInfo, string stagingDirectory, Action <string> errorMessageCallback) { Log.Information(@"Updating mod: " + updateInfo.mod.ModName + @" from " + updateInfo.LocalizedLocalVersionString + @" to " + updateInfo.LocalizedServerVersionString); string modPath = updateInfo.mod.ModPath; string serverRoot = UpdateStorageRoot + updateInfo.serverfolder + '/'; bool cancelDownloading = false; var stagedFileMapping = new ConcurrentDictionary <string, string>(); foreach (var sf in updateInfo.applicableUpdates) { sf.AmountDownloaded = 0; //reset in the event this is a second attempt } // CREATE STAGING CLONES FIRST foreach (var v in updateInfo.cloneOperations) { // Clone file so we don't have to download it. string stagingFile = Path.Combine(stagingDirectory, v.Key.relativefilepath); Directory.CreateDirectory(Directory.GetParent(stagingFile).FullName); var sourceFile = Path.Combine(updateInfo.mod.ModPath, v.Value.RelativeFilepath); Log.Information($@"Cloning file for move/rename/copy delta change: {sourceFile} -> {stagingFile}"); File.Copy(sourceFile, stagingFile, true); stagedFileMapping[stagingFile] = Path.Combine(updateInfo.mod.ModPath, v.Key.relativefilepath); } Parallel.ForEach(updateInfo.applicableUpdates, new ParallelOptions { MaxDegreeOfParallelism = 4 }, (sourcefile) => { if (!cancelDownloading) { void downloadProgressCallback(long received, long totalToReceived) { sourcefile.AmountDownloaded = received; updateInfo.RecalculateAmountDownloaded(); } string fullurl = serverRoot + sourcefile.relativefilepath.Replace('\\', '/') + @".lzma"; var downloadedFile = OnlineContent.DownloadToMemory(fullurl, downloadProgressCallback, sourcefile.lzmahash, true); if (downloadedFile.errorMessage != null && !cancelDownloading) { errorMessageCallback?.Invoke(downloadedFile.errorMessage); cancelDownloading = true; return; } if (cancelDownloading) { return; //Concurrency for long running download to memory } //Hash OK string stagingFile = Path.Combine(stagingDirectory, sourcefile.relativefilepath); Directory.CreateDirectory(Directory.GetParent(stagingFile).FullName); //Decompress file MemoryStream decompressedStream = new MemoryStream(); LZMA.DecompressLZMAStream(downloadedFile.result, decompressedStream); //SevenZipExtractor.DecompressStream(downloadedFile.result, decompressedStream, null, null); //Hash check output if (decompressedStream.Length != sourcefile.size) { Log.Error($@"Decompressed file ({sourcefile.relativefilepath}) is not of correct size. Expected: {sourcefile.size}, got: {decompressedStream.Length}"); errorMessageCallback?.Invoke(M3L.GetString(M3L.string_interp_decompressedFileNotCorrectSize, sourcefile.relativefilepath, sourcefile.size, decompressedStream.Length)); //force localize cancelDownloading = true; return; } var decompressedMD5 = Utilities.CalculateMD5(decompressedStream); if (decompressedMD5 != sourcefile.hash) { Log.Error($@"Decompressed file ({sourcefile.relativefilepath}) has the wrong hash. Expected: {sourcefile.hash}, got: {decompressedMD5}"); errorMessageCallback?.Invoke(M3L.GetString(M3L.string_interp_decompressedFileWrongHash, sourcefile.relativefilepath, sourcefile.hash, decompressedMD5)); //force localize cancelDownloading = true; return; } File.WriteAllBytes(stagingFile, decompressedStream.ToArray()); if (sourcefile.timestamp != 0) { File.SetLastWriteTimeUtc(stagingFile, new DateTime(sourcefile.timestamp)); } Log.Information(@"Wrote updater staged file: " + stagingFile); stagedFileMapping[stagingFile] = Path.Combine(modPath, sourcefile.relativefilepath); } }); if (cancelDownloading) { //callback already should have occured return(false); } //All files have been downloaded successfully. updateInfo.DownloadButtonText = M3L.GetString(M3L.string_applying); //Apply update if (stagedFileMapping.Count > 0) { Log.Information(@"Applying staged update to mod directory"); foreach (var file in stagedFileMapping) { Log.Information($@"Applying update file: {file.Key} => {file.Value}"); Directory.CreateDirectory(Directory.GetParent(file.Value).FullName); File.Copy(file.Key, file.Value, true); } } //Delete files no longer in manifest foreach (var file in updateInfo.filesToDelete) { var fileToDelete = Path.Combine(modPath, file); Log.Information(@"Deleting file for mod update: " + fileToDelete); File.Delete(fileToDelete); } //Delete empty subdirectories Utilities.DeleteEmptySubdirectories(modPath); Utilities.DeleteFilesAndFoldersRecursively(stagingDirectory); //We're done! return(true); }