/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2017 - License: GNU GPLv3 // // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // // rights of fair usage, the disclaimer and warranty conditions. // /////////////////////////////////////////////////////////////////////////// using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Linq; using wasDAVClient; using wasSharp; using wasSharp.Sets; using wasSharpNET.IO.Utilities; using wasStitchNET.Structures; using XML = wasStitchNET.Patchers.XML; namespace wasStitchNET.Repository.Stitching { public class Stitching { /// /// Delegate to subscribe to for stitch progress events. /// /// the sender /// stitch progress event arguments public delegate void StitchProgressEventHandler(object sender, StitchProgressEventArgs e); /// /// Stitch progress event handler. /// public event StitchProgressEventHandler OnProgressUpdate; /// /// Commodity method to raise stitching progress events. /// /// the current stitching status private void StitchProgressUpdate(string status) { // Make sure someone is listening to event if (OnProgressUpdate == null) return; var args = new StitchProgressEventArgs(status); OnProgressUpdate(this, args); } /// /// Stitch! /// /// the was DAV client to use /// the repository URL to stitch from /// the release to stitch to /// the path to the local files to be stitched /// true if files should not be patched /// whether to perform a clean stitching by removing local files /// whether to force stitching repository paths /// true if remote files should not be checked against official checksum /// whether to perform a dryrun run operation without making any changes /// true if stitching completed successfully public async Task Stitch(Client client, string server, string version, string path, bool nopatch = false, bool clean = false, bool force = false, bool noverify = false, bool dryrun = false) { // Set the server. client.Server = server; // The repository path to the version to update. var updateVersionPath = @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, version); // Check that the repository has the requested version. StitchProgressUpdate("Attempting to retrieve remote repository update version folder."); try { if (!client.GetFolder(updateVersionPath).Result.IsCollection) throw new Exception(); } catch (Exception) { throw new StitchException("The repository does not have requested version available."); } // The repository path to the checksum file of the version to update. var updateChecksumPath = string.Join(@"/", updateVersionPath, STITCH_CONSTANTS.UPDATE_CHECKSUM_FILE); // Attempt to retrieve remote checksum file and normalize the hash. StitchProgressUpdate("Retrieving remote repository checksum file."); string updateChecksum; try { using (var stream = client.Download(updateChecksumPath).Result) { using (var reader = new StreamReader(stream)) { // Trim any spaces since we only care about a single-line hash. updateChecksum = Regex.Replace(reader.ReadToEnd(), @"\s+", string.Empty); } } } catch (Exception ex) { throw new StitchException("Unable to retrieve repository checksum file.", ex); } if (string.IsNullOrEmpty(updateChecksum)) throw new StitchException("Empty repository update checksum."); // Hash the remote repository files. StitchProgressUpdate("Hashing remote repository checksum files."); string remoteChecksum; try { remoteChecksum = await Hashing.HashRemoteFiles(client, string.Join(@"/", version, STITCH_CONSTANTS.UPDATE_DATA_PATH)); } catch (Exception ex) { throw new StitchException("Unable to compute remote checksum.", ex); } if (string.IsNullOrEmpty(remoteChecksum)) throw new StitchException("Empty remote checksum."); // Check that the repository checksum file matches the repository file hash. StitchProgressUpdate("Comparing remote repository checksum against remote repository files checksum."); if (!string.Equals(updateChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) throw new StitchException("Repository file checksum mismatch."); // Check that the computed repository file checksum matches the official repository checksum file. if (!noverify) { StitchProgressUpdate("Preparing to verify remote repository file checksum to official checksum."); // Retrieve the official repository checksum file for the requested stitch version. StitchProgressUpdate("Retrieving official repository checksum for requested release version."); string officialChecksum; try { // Point the server to the official server. client.Server = STITCH_CONSTANTS.OFFICIAL_UPDATE_SERVER; using (var stream = client.Download(updateChecksumPath).Result) { using (var reader = new StreamReader(stream)) { // Trim any spaces since we only care about a single-line hash. officialChecksum = Regex.Replace(reader.ReadToEnd(), @"\s+", string.Empty); } } } catch (Exception ex) { throw new StitchException("Unable to retrieve official repository checksum file.", ex); } finally { client.Server = server; } if (string.IsNullOrEmpty(officialChecksum)) throw new StitchException("Unable to retrieve official repository checksum file."); // Compare the official checksum to the repository file checksum. StitchProgressUpdate( $"Comparing official repository checksum ({officialChecksum}) against remote repository files checksum ({remoteChecksum})."); if (!string.Equals(officialChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) throw new StitchException("Repository file checksum does not match official repository checksum."); } var stitchOptions = new StitchOptions(); var optionsPath = @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, version, STITCH_CONSTANTS.UPDATE_OPTIONS_FILE); // Retrieve the repository upgrade options file. StitchProgressUpdate("Retrieving remote repository options."); try { using (var stream = client.Download(optionsPath).Result) { stitchOptions = stitchOptions.Load(stream); } } catch (Exception ex) { throw new StitchException("Unable to retrieve repository options.", ex); } // Retrieve the remote repository release files. StitchProgressUpdate("Retrieving remote repository release files."); var remoteFiles = new HashSet(); try { remoteFiles.UnionWith( Files.LoadRemoteFiles(client, string.Join(@"/", version, STITCH_CONSTANTS.UPDATE_DATA_PATH), @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, version, STITCH_CONSTANTS.UPDATE_DATA_PATH) )); } catch (Exception ex) { throw new StitchException("Unable to download repository release files.", ex); } // Retrieve local path files. StitchProgressUpdate("Retrieving local path files."); var localFiles = new HashSet(); try { localFiles.UnionWith(Files.LoadLocalFiles(path, path, Path.DirectorySeparatorChar)); } catch (Exception ex) { throw new StitchException("Unable to load local files.", ex); } // Files to be wiped. var wipeFiles = new HashSet(); if (clean) switch (stitchOptions.Force || force) { case true: wipeFiles.UnionWith(localFiles.Except(remoteFiles)); break; default: wipeFiles.UnionWith( localFiles.Except(remoteFiles) .Where( o => stitchOptions.FileExcludes.Path.All( p => o.Path.SequenceExcept(p.PathSplit(Path.DirectorySeparatorChar)) .Count() .Equals(o.Path.Count())))); break; } // Files to be stitched. var stitchFiles = new HashSet(); // If the force option was specified then stitch all the files that are not in the remote // repository by ignoring any excludes. switch (stitchOptions.Force || force) { case true: stitchFiles.UnionWith(remoteFiles.Except(localFiles)); break; default: stitchFiles.UnionWith( remoteFiles.Except(localFiles) .Where( o => stitchOptions.FileExcludes.Path.All( p => o.Path.SequenceExcept(p.PathSplit(Path.DirectorySeparatorChar)) .Count() .Equals(o.Path.Count())))); break; } // Wipe local files and directories that have to be removed. StitchProgressUpdate("Removing local files and folders."); var directories = new Queue(); foreach (var file in wipeFiles) { var deletePath = string.Join(Path.DirectorySeparatorChar.ToString(), path, string.Join(Path.DirectorySeparatorChar.ToString(), file.Path)); try { switch (file.PathType) { case StitchPathType.PATH_FILE: if (!dryrun) File.Delete(deletePath); break; case StitchPathType.PATH_DIRECTORY: // we cannot delete the directories right away. directories.Enqueue(deletePath); break; } } catch (Exception ex) { throw new StitchException("Unable remove local files.", ex); } } directories = new Queue(directories.OrderByDescending(o => o)); while (directories.Any()) { var deletePath = directories.Dequeue(); try { if (!dryrun) Directory.Delete(deletePath); } catch (Exception ex) { throw new StitchException("Unable remove local directories.", ex); } } // Stitch files that have to be stitched. StitchProgressUpdate("Stitching files."); foreach (var file in stitchFiles) try { var stitchRemotePath = @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, version, STITCH_CONSTANTS.UPDATE_DATA_PATH, string.Join("/", file.Path)); var stitchLocalPath = string.Join(Path.DirectorySeparatorChar.ToString(), path.PathSplit(Path.DirectorySeparatorChar) .Concat(file.Path)); switch (file.PathType) { case StitchPathType.PATH_DIRECTORY: // Create the directory. if (!dryrun) Directory.CreateDirectory(stitchLocalPath); continue; case StitchPathType.PATH_FILE: // Create the directory to the stitch file. if (!dryrun) Directory.CreateDirectory( string.Join(Path.DirectorySeparatorChar.ToString(), stitchLocalPath.PathSplit(Path.DirectorySeparatorChar).Reverse() .Skip(1) .Reverse())); break; } using (var memoryStream = new MemoryStream()) { using (var stream = client.Download(stitchRemotePath).Result) { stream.CopyTo(memoryStream); } memoryStream.Position = 0L; if (!dryrun) using (var fileStream = IOExtensions.GetWriteStream(stitchLocalPath, FileMode.Create, FileAccess.Write, FileShare.None, STITCH_CONSTANTS.LOCAL_FILE_ACCESS_TIMEOUT)) { memoryStream.CopyTo(fileStream); } } } catch (Exception ex) { throw new StitchException("Unable to stitch files.", ex); } // If no file patches was requested then do not patch and the process is complete. if (nopatch) return true; StitchProgressUpdate("Patching files."); // Retrive working file. var workingFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), path, STITCH_CONSTANTS.WORKING_CONFIGURATION_FILE); StitchProgressUpdate("Parsing working file to be patched."); XDocument workingFile; try { workingFile = XDocument.Load(workingFilePath); } catch (Exception ex) { throw new StitchException("Unable to parse working file to be patched.", ex); } // Retrieve default file. StitchProgressUpdate("Parsing default file to be patched."); XDocument defaultFile; try { defaultFile = XDocument.Load(string.Join(Path.DirectorySeparatorChar.ToString(), path, STITCH_CONSTANTS.DEFAULT_CONFIGURATION_FILE)); } catch (Exception ex) { throw new StitchException("Unable to parse default file to be patched.", ex); } // XPaths to exclude from patching. var excludeXPaths = new HashSet(); if (stitchOptions.ConfigurationExcludes != null) excludeXPaths.UnionWith(stitchOptions.ConfigurationExcludes.Tag); // XPaths to force whilst patching. var forceXPaths = new HashSet(); if (stitchOptions.ConfigurationForce != null) forceXPaths.UnionWith(stitchOptions.ConfigurationForce.Tag); // Patch the file. StitchProgressUpdate("Patching file."); var patchedFile = XML .PatchXDocument(workingFile, defaultFile, forceXPaths, excludeXPaths); if (patchedFile == null) throw new StitchException("Unable to patch XML files."); // Create a backup for the file to be patched. StitchProgressUpdate("Creating a backup of the file to be patched."); try { if (!dryrun) File.Copy(workingFilePath, string.Join(Path.DirectorySeparatorChar.ToString(), path, STITCH_CONSTANTS.BACKUP_CONFIGURATION_FILE), true); } catch (Exception ex) { throw new StitchException("Unable to create patched file backup.", ex); } // Write the patched file. StitchProgressUpdate("Saving the patched file."); try { if (!dryrun) patchedFile.Save(string.Join(Path.DirectorySeparatorChar.ToString(), path, STITCH_CONSTANTS.WORKING_CONFIGURATION_FILE)); } catch (Exception ex) { throw new StitchException("Unable to save patched file.", ex); } StitchProgressUpdate("Stitching successful."); return true; } } }