/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; |
} |
} |
} |