/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.Sets; |
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, 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<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, 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; |
} |
} |
} |
/Repository/Files.cs |
@@ -0,0 +1,122 @@ |
/////////////////////////////////////////////////////////////////////////// |
// 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.Security.Cryptography; |
using System.Threading.Tasks; |
using wasDAVClient; |
using wasSharp; |
using wasSharp.Sets; |
using wasSharp.Web.Utilities; |
using wasSharpNET.IO.Utilities; |
using wasStitchNET.Structures; |
|
namespace wasStitchNET.Repository |
{ |
public class Files |
{ |
/// <summary> |
/// Load all files off a remote repository and return them as a stitchable file. |
/// </summary> |
/// <param name="client">the wasDAVClient to use</param> |
/// <param name="path">the web path to retrieve the files from</param> |
/// <param name="basePath">the base path of the repository</param> |
/// <param name="pathSeparator">the DAV separator character</param> |
/// <returns>a collection of stitch files</returns> |
public static IEnumerable<StitchFile> LoadRemoteFiles(Client client, string path, string basePath, |
char pathSeparator = '/') |
{ |
Func<string, Task<StitchFile>> getStitchFile = async file => |
{ |
using (var sha1managed = new SHA1Managed()) |
{ |
using (var stream = await client.Download(file)) |
{ |
//fileHash = sha1managed.ComputeHash(stream); |
return new StitchFile |
{ |
Path = |
file.PathSplit(pathSeparator) |
.Select(WebExtensions.URLUnescapeDataString) |
.SequenceExcept( |
basePath.PathSplit(pathSeparator)), |
SHA1 = sha1managed.ComputeHash(stream).ToHexString(), |
PathType = StitchPathType.PATH_FILE |
}; |
} |
} |
}; |
|
foreach (var item in client.List(path).Result) |
switch (item.IsCollection) |
{ |
case true: |
var directoryPath = item.Href.TrimEnd(pathSeparator).PathSplit(pathSeparator) |
.Select(WebExtensions.URLUnescapeDataString) |
.SequenceExcept( |
basePath.PathSplit(pathSeparator)); |
yield return new StitchFile |
{ |
Path = directoryPath, |
SHA1 = string.Empty, |
PathType = StitchPathType.PATH_DIRECTORY |
}; |
foreach (var file in LoadRemoteFiles(client, item.Href, basePath)) |
yield return file; |
break; |
|
default: |
yield return getStitchFile(item.Href).Result; |
break; |
} |
} |
|
/// <summary> |
/// Load all files from a local path and return them as a stitchable file. |
/// </summary> |
/// <param name="path">the local path to files</param> |
/// <param name="basePath">the base path to the folder</param> |
/// <param name="pathSeparator">the local filesystem separator to use</param> |
/// <returns>a collection of stitch files</returns> |
public static IEnumerable<StitchFile> LoadLocalFiles(string path, string basePath, char pathSeparator = '\\') |
{ |
foreach (var file in Directory.GetFiles(path)) |
using (var sha1managed = new SHA1Managed()) |
{ |
using (var fileStream = IOExtensions.GetWriteStream(file, FileMode.Open, |
FileAccess.Read, FileShare.Read, STITCH_CONSTANTS.LOCAL_FILE_ACCESS_TIMEOUT)) |
{ |
yield return new StitchFile |
{ |
Path = |
file.PathSplit(pathSeparator) |
.SequenceExcept( |
basePath.PathSplit(pathSeparator)), |
SHA1 = sha1managed.ComputeHash(fileStream).ToHexString(), |
PathType = StitchPathType.PATH_FILE |
}; |
} |
} |
foreach (var directory in Directory.GetDirectories(path)) |
{ |
var directoryPath = directory.PathSplit(pathSeparator) |
.SequenceExcept( |
basePath.PathSplit(pathSeparator)); |
yield return new StitchFile |
{ |
Path = directoryPath, |
SHA1 = string.Empty, |
PathType = StitchPathType.PATH_DIRECTORY |
}; |
foreach (var file in LoadLocalFiles(directory, basePath, pathSeparator)) |
yield return file; |
} |
} |
} |
} |
/Repository/Tools.cs |
@@ -10,9 +10,12 @@ |
using System.Net; |
using System.Threading.Tasks; |
using wasDAVClient; |
using wasDAVClient.Helpers; |
using wasSharp; |
using wasSharp.Linq; |
using wasSharp.Sets; |
using wasSharp.Web.Utilities; |
using wasSharpNET.Cryptography; |
using SHA1 = System.Security.Cryptography.SHA1; |
|
namespace wasStitchNET.Repository |
{ |
@@ -27,6 +30,8 @@ |
/// <returns>a list of Corrade files contained in the repository</returns> |
public static async Task<IEnumerable<string>> GetReleaseFiles(string server, Version release, int timeout) |
{ |
try |
{ |
using (var client = new Client(new NetworkCredential()) |
{ |
Timeout = timeout, |
@@ -48,12 +53,11 @@ |
.Select(o => string.Join(Constants.DIRECTORY_SEPARATOR, |
o.Href.PathSplit(Constants.DIRECTORY_SEPARATOR.First()) |
.Select(part => |
WebExtensions.URLUnescapeDataString( |
part.Trim(Constants.DIRECTORY_SEPARATOR.First()) |
part.Trim(Constants.DIRECTORY_SEPARATOR.First()).URLUnescapeDataString() |
) |
) |
.Where(part => !string.IsNullOrEmpty(part)) |
.SequenceExceptAny(new[] { |
.SequenceExceptAny(new[] |
{ |
STITCH_CONSTANTS.UPDATE_PATH, |
STITCH_CONSTANTS.PROGRESSIVE_PATH, |
$"{release.Major}.{release.Minor}", |
@@ -63,5 +67,64 @@ |
); |
} |
} |
catch (wasDAVConflictException ex) |
{ |
throw new StitchException(ex); |
} |
} |
|
/// <summary> |
/// Lists the release files for a given release version. |
/// </summary> |
/// <param name="server">the Stitch server to use</param> |
/// <param name="release">the release to list files for</param> |
/// <param name="timeout">the timeout connecting to the Stitch repository</param> |
/// <returns>a list of Corrade files contained in the repository</returns> |
public static IEnumerable<KeyValuePair<string, string>> GetReleaseFileHashes(string server, Version release, |
int timeout) |
{ |
try |
{ |
using (var client = new Client(new NetworkCredential()) |
{ |
Timeout = timeout, |
UserAgent = STITCH_CONSTANTS.USER_AGENT.Product.Name, |
UserAgentVersion = STITCH_CONSTANTS.USER_AGENT.Product.Version, |
Server = server, |
BasePath = string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, |
STITCH_CONSTANTS.PROGRESSIVE_PATH) |
}) |
{ |
return client |
.List( |
string.Join( |
Constants.DIRECTORY_SEPARATOR, |
$"{release.Major}.{release.Minor}", |
STITCH_CONSTANTS.UPDATE_DATA_PATH), |
Constants.DavDepth.ALL) |
.Result |
.AsParallel() |
.Where(item => !item.IsCollection) |
.ToDictionary(o => string.Join(Constants.DIRECTORY_SEPARATOR, |
o.Href.PathSplit(Constants.DIRECTORY_SEPARATOR.First()) |
.Select(part => part.Trim(Constants.DIRECTORY_SEPARATOR.First()) |
.URLUnescapeDataString() |
) |
.Where(part => !string.IsNullOrEmpty(part)) |
.SequenceExceptAny(new[] |
{ |
STITCH_CONSTANTS.UPDATE_PATH, |
STITCH_CONSTANTS.PROGRESSIVE_PATH, |
$"{release.Major}.{release.Minor}", |
STITCH_CONSTANTS.UPDATE_DATA_PATH |
})), |
o => SHA1.Create().ToHex(client.Download(o.Href).Result)); |
} |
} |
catch (wasDAVException ex) |
{ |
throw new StitchException(ex); |
} |
} |
} |
} |
/Repository/Hashing.cs |
@@ -0,0 +1,105 @@ |
/////////////////////////////////////////////////////////////////////////// |
// Copyright (C) Wizardry and Steamworks 2016 - 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.Collections.Generic; |
using System.IO; |
using System.Linq; |
using System.Net; |
using System.Security.Cryptography; |
using System.Threading.Tasks; |
using wasDAVClient; |
using wasDAVClient.Model; |
using wasSharp; |
using wasSharpNET.IO.Utilities; |
|
namespace wasStitchNET.Repository |
{ |
public class Hashing |
{ |
/// <summary> |
/// Hash local files inside a directory. |
/// </summary> |
/// <param name="path">the path to a directory to hash</param> |
/// <param name="bufferSize">the partial hash buffer (default: 16384)</param> |
/// <returns>a SHA1 hash of all files in a directory ordered by name</returns> |
public static async Task<string> HashLocalFiles(string path, int bufferSize = 16384) |
{ |
return await new Task<string>(() => |
{ |
using (var sha1managed = new SHA1Managed()) |
{ |
foreach (var stream in Directory |
.GetFiles(path, "*.*", SearchOption.AllDirectories) |
.OrderBy(o => o.Split(Path.DirectorySeparatorChar).Last()) |
.Select(file => IOExtensions.GetWriteStream(file, FileMode.Open, |
FileAccess.Read, FileShare.Read, STITCH_CONSTANTS.LOCAL_FILE_ACCESS_TIMEOUT))) |
using (var binaryReader = new BinaryReader(stream)) |
{ |
var buff = new byte[bufferSize]; |
int read; |
while ((read = binaryReader.Read(buff, 0, buff.Length)) != 0) |
sha1managed.TransformBlock(buff, 0, read, null, 0); |
} |
|
sha1managed.TransformFinalBlock(new byte[] { }, 0, 0); |
return sha1managed.Hash.ToHexString(); |
} |
}); |
} |
|
/// <summary> |
/// Hash the files of a Stitch remote repository release. |
/// </summary> |
/// <param name="server">the server to hash the release for</param> |
/// <param name="update">the Stitch release to hash</param> |
/// <param name="timeout">the timeout in milliseconds to allow the server to respond</param> |
/// <returns>a SHA1 hash of all files in the release directory ordered by name</returns> |
public static async Task<string> HashRemoteFiles(string server, string update, int timeout) |
{ |
using (var client = new Client(new NetworkCredential()) |
{ |
Timeout = timeout, |
UserAgent = STITCH_CONSTANTS.USER_AGENT.Product.Name, |
UserAgentVersion = STITCH_CONSTANTS.USER_AGENT.Product.Version, |
Server = server, |
BasePath = string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, |
STITCH_CONSTANTS.PROGRESSIVE_PATH) |
}) |
{ |
return $"{await HashRemoteFiles(client, string.Join(@"/", update, STITCH_CONSTANTS.UPDATE_DATA_PATH))}"; |
} |
} |
|
/// <summary> |
/// Hash the files of a Stitch remote repository release using an existing was DAV client. |
/// </summary> |
/// <param name="client">the was DAV client to use</param> |
/// <param name="path">the path to the repository release folder</param> |
/// <param name="bufferSize">the partial hash buffer (default: 16384)</param> |
/// <returns>a SHA1 hash of all files in the release directory ordered by name</returns> |
public static async Task<string> HashRemoteFiles(Client client, string path, int bufferSize = 16384) |
{ |
using (var sha1managed = new SHA1Managed()) |
{ |
//var list = new List<Item>(); |
foreach (var stream in (await client.List(path, Constants.DavDepth.ALL)) |
.OrderBy(o => o.DisplayName) |
.Where(item => !item.IsCollection) |
.Select(file => client.Download(file.Href))) |
using (var binaryReader = new BinaryReader(await stream)) |
{ |
var buff = new byte[bufferSize]; |
int read; |
while ((read = binaryReader.Read(buff, 0, buff.Length)) != 0) |
sha1managed.TransformBlock(buff, 0, read, null, 0); |
} |
|
sha1managed.TransformFinalBlock(new byte[] { }, 0, 0); |
return sha1managed.Hash.ToHexString(); |
} |
} |
} |
} |
/Repository/Mirrors.cs |
@@ -0,0 +1,143 @@ |
/////////////////////////////////////////////////////////////////////////// |
// 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.Net; |
using MaxMind.GeoIP2.Responses; |
using wasDAVClient; |
using wasSharp.Geo; |
using wasStitchNET.GeoIP; |
using wasStitchNET.Structures; |
|
namespace wasStitchNET.Repository |
{ |
public static class Mirrors |
{ |
/// <summary> |
/// Returns a list of valid Stitch mirrors. |
/// </summary> |
/// <param name="localCity">the local city from where the method is invoked</param> |
/// <param name="mirror">the mirror to get the distance to</param> |
/// <param name="client">the was DAV client to use</param> |
/// <param name="server">the Stitch server to query</param> |
/// <returns>a list of discovered Stitch mirrors</returns> |
public static IEnumerable<StitchMirror> InitializeMirrors(CityResponse localCity, Client client, string server) |
{ |
Func<string, StitchMirror> computeMirrorDistance = o => |
{ |
var mirrorDistance = GetMirrorDistance(localCity, o); |
if (mirrorDistance.Equals(default(KeyValuePair<string, Distance>))) |
return default(StitchMirror); |
return new StitchMirror(mirrorDistance.Key, mirrorDistance.Value); |
}; |
|
var mirrors = new List<string> {server}; |
yield return computeMirrorDistance(server); |
|
foreach (var stitchMirror in mirrors.AsParallel().SelectMany( |
mirror => FetchStitchMirrors(client, mirror) |
.AsParallel() |
.Where(o => !string.IsNullOrEmpty(o) && !mirrors.Contains(o)) |
.Select(computeMirrorDistance))) |
{ |
if (stitchMirror.Equals(default(StitchMirror))) |
continue; |
mirrors.Add(stitchMirror.Address); |
yield return stitchMirror; |
} |
} |
|
/// <summary> |
/// Retrieves a list of mirrors from a Stitch repository. |
/// </summary> |
/// <param name="client">the was DAV client to use</param> |
/// <param name="server">the Stitch server to retrieve the mirrors from</param> |
/// <returns>a list of mirrors</returns> |
public static IEnumerable<string> FetchStitchMirrors(Client client, string server) |
{ |
// Set the server. |
client.Server = server; |
|
// Set the mirror path |
var mirrorsPath = @"/" + |
string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, |
STITCH_CONSTANTS.UPDATE_MIRRORS_FILE); |
|
using (var stream = client.Download(mirrorsPath).Result) |
{ |
if (stream == null) |
yield break; |
|
using (var streamReader = new StreamReader(stream)) |
{ |
string mirror; |
do |
{ |
mirror = streamReader.ReadLine(); |
|
if (string.IsNullOrEmpty(mirror)) |
continue; |
|
yield return mirror; |
} while (!string.IsNullOrEmpty(mirror)); |
} |
} |
} |
|
/// <summary> |
/// Gets the distance to a mirror. |
/// </summary> |
/// <param name="localCity">the local city from where the method is invoked</param> |
/// <param name="mirror">the mirror to get the distance to</param> |
/// <returns>a key-value pair of mirror by distance</returns> |
public static KeyValuePair<string, Distance> GetMirrorDistance(CityResponse localCity, string mirror) |
{ |
// Check that the mirror has a proper URI. |
Uri mirrorUri; |
if (!Uri.TryCreate(mirror, UriKind.Absolute, out mirrorUri)) |
return default(KeyValuePair<string, Distance>); |
|
// If we do not know the local city, then just return the mirror. |
if (localCity == null) |
return new KeyValuePair<string, Distance>(mirror, null); |
|
// Resolve the mirror hostname to an IP address. |
IPAddress address; |
try |
{ |
address = Dns.GetHostAddresses(mirrorUri.Host).FirstOrDefault(); |
} |
catch (Exception) |
{ |
return |
new KeyValuePair<string, Distance>(mirror, null); |
} |
|
// Resolve the IP address to a city response. |
var remoteCity = address.GeoIPGetCity(); |
if (remoteCity == null) |
return new KeyValuePair<string, Distance>(mirror, null); |
|
// Compute the distance to the mirror. |
switch (remoteCity.Location.HasCoordinates) |
{ |
case true: |
var local = new GeographicCoordinate(localCity.Location.Latitude.Value, |
localCity.Location.Longitude.Value); |
var remote = new GeographicCoordinate(remoteCity.Location.Latitude.Value, |
remoteCity.Location.Longitude.Value); |
return |
new KeyValuePair<string, Distance>(mirror, local.HaversineDistanceTo(remote)); |
default: |
return |
new KeyValuePair<string, Distance>(mirror, null); |
} |
} |
} |
} |
/Repository/StitchOptions.cs |
@@ -0,0 +1,82 @@ |
/////////////////////////////////////////////////////////////////////////// |
// Copyright (C) Wizardry and Steamworks 2013 - 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.Collections.Generic; |
using System.IO; |
using System.Xml.Serialization; |
using wasSharp; |
|
namespace wasStitchNET.Repository |
{ |
[XmlRoot(ElementName = "FileExcludes")] |
public class FileExcludes |
{ |
[XmlElement(ElementName = "Path")] |
public HashSet<string> Path { get; set; } |
} |
|
[XmlRoot(ElementName = "ConfigurationExcludes")] |
public class ConfigurationExcludes |
{ |
[XmlElement(ElementName = "Path")] |
public HashSet<string> Tag { get; set; } |
} |
|
[XmlRoot(ElementName = "ConfigurationForce")] |
public class ConfigurationForce |
{ |
[XmlElement(ElementName = "Path")] |
public HashSet<string> Tag { get; set; } |
} |
|
[XmlRoot(ElementName = "StitchOptions")] |
public class StitchOptions |
{ |
[Reflection.NameAttribute("force")] |
[XmlElement(ElementName = "force")] |
public bool Force { get; set; } |
|
[Reflection.NameAttribute("FileExcludes")] |
[XmlElement(ElementName = "FileExcludes")] |
public FileExcludes FileExcludes { get; set; } |
|
[Reflection.NameAttribute("ConfigurationExcludes")] |
[XmlElement(ElementName = "ConfigurationExcludes")] |
public ConfigurationExcludes ConfigurationExcludes { get; set; } |
|
[Reflection.NameAttribute("ConfigurationForce")] |
[XmlElement(ElementName = "ConfigurationForce")] |
public ConfigurationExcludes ConfigurationForce { get; set; } |
|
[XmlAttribute(AttributeName = "xsi", Namespace = "http://www.w3.org/2000/xmlns/")] |
public string Xsi { get; set; } |
|
[XmlAttribute(AttributeName = "xsd", Namespace = "http://www.w3.org/2000/xmlns/")] |
public string Xsd { get; set; } |
} |
|
public static class StitchOptionsSerialization |
{ |
public static StitchOptions Load(this StitchOptions stitchOptions, Stream stream) |
{ |
using (var streamReader = new StreamReader(stream)) |
{ |
var serializer = new XmlSerializer(typeof(StitchOptions)); |
return (StitchOptions) serializer.Deserialize(streamReader); |
} |
} |
|
public static string ToXML(this StitchOptions stitchOptions) |
{ |
using (var writer = new StringWriter()) |
{ |
var serializer = new XmlSerializer(typeof(StitchOptions)); |
serializer.Serialize(writer, stitchOptions); |
return writer.ToString(); |
} |
} |
} |
} |