wasStitchNET

Subversion Repositories:
Compare Path: Rev
With Path: Rev
?path1? @ 12  →  ?path2? @ 13
/Repository/Stitching/StitchProgressEvent.cs
@@ -0,0 +1,20 @@
///////////////////////////////////////////////////////////////////////////
// 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;
 
namespace wasStitchNET.Repository.Stitching
{
public class StitchProgressEventArgs : EventArgs
{
public StitchProgressEventArgs(string status)
{
Status = status;
}
 
public string Status { get; }
}
}
/Repository/Stitching/Stitching.cs
@@ -0,0 +1,447 @@
///////////////////////////////////////////////////////////////////////////
// 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;
}
}
}