wasStitchNET – Rev 13

Subversion Repositories:
Rev:
///////////////////////////////////////////////////////////////////////////
//  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.Linq;
using wasSharpNET.IO.Utilities;
using wasStitchNET.Structures;
using XML = wasStitchNET.Patchers.XML;

namespace wasStitchNET.Repository.Stitching
{
    public class Stitching
    {
        /// <summary>
        ///     Delegate to subscribe to for stitch progress events.
        /// </summary>
        /// <param name="sender">the sender</param>
        /// <param name="e">stitch progress event arguments</param>
        public delegate void StitchProgressEventHandler(object sender, StitchProgressEventArgs e);

        /// <summary>
        ///     Stitch progress event handler.
        /// </summary>
        public event StitchProgressEventHandler OnProgressUpdate;

        /// <summary>
        ///     Commodity method to raise stitching progress events.
        /// </summary>
        /// <param name="status">the current stitching status</param>
        private void StitchProgressUpdate(string status)
        {
            // Make sure someone is listening to event
            if (OnProgressUpdate == null) return;

            var args = new StitchProgressEventArgs(status);
            OnProgressUpdate(this, args);
        }

        /// <summary>
        ///     Stitch!
        /// </summary>
        /// <param name="client">the was DAV client to use</param>
        /// <param name="server">the repository URL to stitch from</param>
        /// <param name="version">the release to stitch to</param>
        /// <param name="path">the path to the local files to be stitched</param>
        /// <param name="nopatch">true if files should not be patched</param>
        /// <param name="clean">whether to perform a clean stitching by removing local files</param>
        /// <param name="force">whether to force stitching repository paths</param>
        /// <param name="noverify">true if remote files should not be checked against official checksum</param>
        /// <param name="dryrun">whether to perform a dryrun run operation without making any changes</param>
        /// <returns>true if stitching completed successfully</returns>
        public async Task<bool> 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<StitchFile>();
            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<StitchFile>();
            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<StitchFile>();
            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<StitchFile>();
            // 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<string>();
            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<string>(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, @"Corrade.ini");

            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, @"Corrade.ini.default"));
            }
            catch (Exception ex)
            {
                throw new StitchException("Unable to parse default file to be patched.", ex);
            }

            // XPaths to exclude from patching.
            var excludeXPaths = new HashSet<string>();
            if (stitchOptions.ConfigurationExcludes != null)
                excludeXPaths.UnionWith(stitchOptions.ConfigurationExcludes.Tag);

            // XPaths to force whilst patching.
            var forceXPaths = new HashSet<string>();
            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, @"Corrade.ini.bak"), 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, @"Corrade.ini"));
            }
            catch (Exception ex)
            {
                throw new StitchException("Unable to save patched file.", ex);
            }

            StitchProgressUpdate("Stitching successful.");
            return true;
        }
    }
}

Generated by GNU Enscript 1.6.5.90.