wasStitchNET – Blame information for rev 21
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
13 | office | 1 | /////////////////////////////////////////////////////////////////////////// |
2 | // Copyright (C) Wizardry and Steamworks 2017 - License: GNU GPLv3 // |
||
3 | // Please see: http://www.gnu.org/licenses/gpl.html for legal details, // |
||
4 | // rights of fair usage, the disclaimer and warranty conditions. // |
||
5 | /////////////////////////////////////////////////////////////////////////// |
||
6 | |||
7 | using System; |
||
8 | using System.Collections.Generic; |
||
9 | using System.IO; |
||
10 | using System.Linq; |
||
11 | using System.Text.RegularExpressions; |
||
12 | using System.Threading.Tasks; |
||
13 | using System.Xml.Linq; |
||
14 | using wasDAVClient; |
||
15 | using wasSharp; |
||
20 | office | 16 | using wasSharp.Sets; |
13 | office | 17 | using wasSharpNET.IO.Utilities; |
18 | using wasStitchNET.Structures; |
||
19 | using XML = wasStitchNET.Patchers.XML; |
||
20 | |||
21 | namespace wasStitchNET.Repository.Stitching |
||
22 | { |
||
23 | public class Stitching |
||
24 | { |
||
25 | /// <summary> |
||
26 | /// Delegate to subscribe to for stitch progress events. |
||
27 | /// </summary> |
||
28 | /// <param name="sender">the sender</param> |
||
29 | /// <param name="e">stitch progress event arguments</param> |
||
30 | public delegate void StitchProgressEventHandler(object sender, StitchProgressEventArgs e); |
||
31 | |||
32 | /// <summary> |
||
33 | /// Stitch progress event handler. |
||
34 | /// </summary> |
||
35 | public event StitchProgressEventHandler OnProgressUpdate; |
||
36 | |||
37 | /// <summary> |
||
38 | /// Commodity method to raise stitching progress events. |
||
39 | /// </summary> |
||
40 | /// <param name="status">the current stitching status</param> |
||
41 | private void StitchProgressUpdate(string status) |
||
42 | { |
||
43 | // Make sure someone is listening to event |
||
44 | if (OnProgressUpdate == null) return; |
||
45 | |||
46 | var args = new StitchProgressEventArgs(status); |
||
47 | OnProgressUpdate(this, args); |
||
48 | } |
||
49 | |||
50 | /// <summary> |
||
51 | /// Stitch! |
||
52 | /// </summary> |
||
53 | /// <param name="client">the was DAV client to use</param> |
||
54 | /// <param name="server">the repository URL to stitch from</param> |
||
55 | /// <param name="version">the release to stitch to</param> |
||
56 | /// <param name="path">the path to the local files to be stitched</param> |
||
57 | /// <param name="nopatch">true if files should not be patched</param> |
||
58 | /// <param name="clean">whether to perform a clean stitching by removing local files</param> |
||
59 | /// <param name="force">whether to force stitching repository paths</param> |
||
60 | /// <param name="noverify">true if remote files should not be checked against official checksum</param> |
||
61 | /// <param name="dryrun">whether to perform a dryrun run operation without making any changes</param> |
||
62 | /// <returns>true if stitching completed successfully</returns> |
||
63 | public async Task<bool> Stitch(Client client, string server, string version, string path, |
||
64 | bool nopatch = false, |
||
65 | bool clean = false, bool force = false, bool noverify = false, bool dryrun = false) |
||
66 | { |
||
67 | // Set the server. |
||
68 | client.Server = server; |
||
69 | |||
70 | // The repository path to the version to update. |
||
71 | var updateVersionPath = @"/" + |
||
72 | string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, |
||
73 | STITCH_CONSTANTS.PROGRESSIVE_PATH, |
||
74 | version); |
||
75 | // Check that the repository has the requested version. |
||
76 | StitchProgressUpdate("Attempting to retrieve remote repository update version folder."); |
||
77 | try |
||
78 | { |
||
79 | if (!client.GetFolder(updateVersionPath).Result.IsCollection) |
||
80 | throw new Exception(); |
||
81 | } |
||
82 | catch (Exception) |
||
83 | { |
||
84 | throw new StitchException("The repository does not have requested version available."); |
||
85 | } |
||
86 | |||
87 | // The repository path to the checksum file of the version to update. |
||
88 | var updateChecksumPath = string.Join(@"/", updateVersionPath, STITCH_CONSTANTS.UPDATE_CHECKSUM_FILE); |
||
89 | |||
90 | // Attempt to retrieve remote checksum file and normalize the hash. |
||
91 | StitchProgressUpdate("Retrieving remote repository checksum file."); |
||
92 | string updateChecksum; |
||
93 | try |
||
94 | { |
||
95 | using (var stream = client.Download(updateChecksumPath).Result) |
||
96 | { |
||
97 | using (var reader = new StreamReader(stream)) |
||
98 | { |
||
99 | // Trim any spaces since we only care about a single-line hash. |
||
100 | updateChecksum = Regex.Replace(reader.ReadToEnd(), @"\s+", string.Empty); |
||
101 | } |
||
102 | } |
||
103 | } |
||
104 | catch (Exception ex) |
||
105 | { |
||
106 | throw new StitchException("Unable to retrieve repository checksum file.", ex); |
||
107 | } |
||
108 | |||
109 | if (string.IsNullOrEmpty(updateChecksum)) |
||
110 | throw new StitchException("Empty repository update checksum."); |
||
111 | |||
112 | // Hash the remote repository files. |
||
113 | StitchProgressUpdate("Hashing remote repository checksum files."); |
||
114 | string remoteChecksum; |
||
115 | try |
||
116 | { |
||
117 | remoteChecksum = await Hashing.HashRemoteFiles(client, |
||
118 | string.Join(@"/", version, STITCH_CONSTANTS.UPDATE_DATA_PATH)); |
||
119 | } |
||
120 | catch (Exception ex) |
||
121 | { |
||
122 | throw new StitchException("Unable to compute remote checksum.", ex); |
||
123 | } |
||
124 | |||
125 | if (string.IsNullOrEmpty(remoteChecksum)) |
||
126 | throw new StitchException("Empty remote checksum."); |
||
127 | |||
128 | // Check that the repository checksum file matches the repository file hash. |
||
129 | StitchProgressUpdate("Comparing remote repository checksum against remote repository files checksum."); |
||
130 | if (!string.Equals(updateChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) |
||
131 | throw new StitchException("Repository file checksum mismatch."); |
||
132 | |||
133 | // Check that the computed repository file checksum matches the official repository checksum file. |
||
134 | if (!noverify) |
||
135 | { |
||
136 | StitchProgressUpdate("Preparing to verify remote repository file checksum to official checksum."); |
||
137 | |||
138 | // Retrieve the official repository checksum file for the requested stitch version. |
||
139 | StitchProgressUpdate("Retrieving official repository checksum for requested release version."); |
||
140 | string officialChecksum; |
||
141 | try |
||
142 | { |
||
143 | // Point the server to the official server. |
||
144 | client.Server = STITCH_CONSTANTS.OFFICIAL_UPDATE_SERVER; |
||
145 | using (var stream = client.Download(updateChecksumPath).Result) |
||
146 | { |
||
147 | using (var reader = new StreamReader(stream)) |
||
148 | { |
||
149 | // Trim any spaces since we only care about a single-line hash. |
||
150 | officialChecksum = Regex.Replace(reader.ReadToEnd(), @"\s+", string.Empty); |
||
151 | } |
||
152 | } |
||
153 | } |
||
154 | catch (Exception ex) |
||
155 | { |
||
156 | throw new StitchException("Unable to retrieve official repository checksum file.", ex); |
||
157 | } |
||
158 | finally |
||
159 | { |
||
160 | client.Server = server; |
||
161 | } |
||
162 | |||
163 | if (string.IsNullOrEmpty(officialChecksum)) |
||
164 | throw new StitchException("Unable to retrieve official repository checksum file."); |
||
165 | |||
166 | // Compare the official checksum to the repository file checksum. |
||
167 | StitchProgressUpdate( |
||
168 | $"Comparing official repository checksum ({officialChecksum}) against remote repository files checksum ({remoteChecksum})."); |
||
169 | if (!string.Equals(officialChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) |
||
170 | throw new StitchException("Repository file checksum does not match official repository checksum."); |
||
171 | } |
||
172 | |||
173 | |||
174 | var stitchOptions = new StitchOptions(); |
||
175 | var optionsPath = @"/" + |
||
176 | string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, |
||
177 | version, STITCH_CONSTANTS.UPDATE_OPTIONS_FILE); |
||
178 | |||
179 | // Retrieve the repository upgrade options file. |
||
180 | StitchProgressUpdate("Retrieving remote repository options."); |
||
181 | try |
||
182 | { |
||
183 | using (var stream = client.Download(optionsPath).Result) |
||
184 | { |
||
185 | stitchOptions = stitchOptions.Load(stream); |
||
186 | } |
||
187 | } |
||
188 | catch (Exception ex) |
||
189 | { |
||
190 | throw new StitchException("Unable to retrieve repository options.", ex); |
||
191 | } |
||
192 | |||
193 | // Retrieve the remote repository release files. |
||
194 | StitchProgressUpdate("Retrieving remote repository release files."); |
||
195 | var remoteFiles = new HashSet<StitchFile>(); |
||
196 | try |
||
197 | { |
||
198 | remoteFiles.UnionWith( |
||
199 | Files.LoadRemoteFiles(client, |
||
200 | string.Join(@"/", version, STITCH_CONSTANTS.UPDATE_DATA_PATH), |
||
201 | @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, STITCH_CONSTANTS.PROGRESSIVE_PATH, |
||
202 | version, STITCH_CONSTANTS.UPDATE_DATA_PATH) |
||
203 | )); |
||
204 | } |
||
205 | catch (Exception ex) |
||
206 | { |
||
207 | throw new StitchException("Unable to download repository release files.", ex); |
||
208 | } |
||
209 | |||
210 | // Retrieve local path files. |
||
211 | StitchProgressUpdate("Retrieving local path files."); |
||
212 | var localFiles = new HashSet<StitchFile>(); |
||
213 | try |
||
214 | { |
||
215 | localFiles.UnionWith(Files.LoadLocalFiles(path, |
||
216 | path, |
||
217 | Path.DirectorySeparatorChar)); |
||
218 | } |
||
219 | catch (Exception ex) |
||
220 | { |
||
221 | throw new StitchException("Unable to load local files.", ex); |
||
222 | } |
||
223 | |||
224 | // Files to be wiped. |
||
225 | var wipeFiles = new HashSet<StitchFile>(); |
||
226 | if (clean) |
||
227 | switch (stitchOptions.Force || force) |
||
228 | { |
||
229 | case true: |
||
230 | wipeFiles.UnionWith(localFiles.Except(remoteFiles)); |
||
231 | break; |
||
232 | |||
233 | default: |
||
234 | wipeFiles.UnionWith( |
||
235 | localFiles.Except(remoteFiles) |
||
236 | .Where( |
||
237 | o => |
||
238 | stitchOptions.FileExcludes.Path.All( |
||
239 | p => |
||
240 | o.Path.SequenceExcept(p.PathSplit(Path.DirectorySeparatorChar)) |
||
241 | .Count() |
||
242 | .Equals(o.Path.Count())))); |
||
243 | break; |
||
244 | } |
||
245 | |||
246 | // Files to be stitched. |
||
247 | var stitchFiles = new HashSet<StitchFile>(); |
||
248 | // If the force option was specified then stitch all the files that are not in the remote |
||
249 | // repository by ignoring any excludes. |
||
250 | switch (stitchOptions.Force || force) |
||
251 | { |
||
252 | case true: |
||
253 | stitchFiles.UnionWith(remoteFiles.Except(localFiles)); |
||
254 | break; |
||
255 | |||
256 | default: |
||
257 | stitchFiles.UnionWith( |
||
258 | remoteFiles.Except(localFiles) |
||
259 | .Where( |
||
260 | o => |
||
261 | stitchOptions.FileExcludes.Path.All( |
||
262 | p => |
||
263 | o.Path.SequenceExcept(p.PathSplit(Path.DirectorySeparatorChar)) |
||
264 | .Count() |
||
265 | .Equals(o.Path.Count())))); |
||
266 | break; |
||
267 | } |
||
268 | |||
269 | // Wipe local files and directories that have to be removed. |
||
270 | StitchProgressUpdate("Removing local files and folders."); |
||
271 | var directories = new Queue<string>(); |
||
272 | foreach (var file in wipeFiles) |
||
273 | { |
||
274 | var deletePath = string.Join(Path.DirectorySeparatorChar.ToString(), |
||
275 | path, |
||
276 | string.Join(Path.DirectorySeparatorChar.ToString(), file.Path)); |
||
277 | try |
||
278 | { |
||
279 | switch (file.PathType) |
||
280 | { |
||
281 | case StitchPathType.PATH_FILE: |
||
282 | if (!dryrun) |
||
283 | File.Delete(deletePath); |
||
284 | break; |
||
285 | |||
286 | case StitchPathType.PATH_DIRECTORY: // we cannot delete the directories right away. |
||
287 | directories.Enqueue(deletePath); |
||
288 | break; |
||
289 | } |
||
290 | } |
||
291 | catch (Exception ex) |
||
292 | { |
||
293 | throw new StitchException("Unable remove local files.", ex); |
||
294 | } |
||
295 | } |
||
296 | |||
297 | directories = new Queue<string>(directories.OrderByDescending(o => o)); |
||
298 | |||
299 | while (directories.Any()) |
||
300 | { |
||
301 | var deletePath = directories.Dequeue(); |
||
302 | try |
||
303 | { |
||
304 | if (!dryrun) |
||
305 | Directory.Delete(deletePath); |
||
306 | } |
||
307 | catch (Exception ex) |
||
308 | { |
||
309 | throw new StitchException("Unable remove local directories.", ex); |
||
310 | } |
||
311 | } |
||
312 | |||
313 | // Stitch files that have to be stitched. |
||
314 | StitchProgressUpdate("Stitching files."); |
||
315 | foreach (var file in stitchFiles) |
||
316 | try |
||
317 | { |
||
318 | var stitchRemotePath = @"/" + string.Join(@"/", STITCH_CONSTANTS.UPDATE_PATH, |
||
319 | STITCH_CONSTANTS.PROGRESSIVE_PATH, |
||
320 | version, STITCH_CONSTANTS.UPDATE_DATA_PATH, |
||
321 | string.Join("/", file.Path)); |
||
322 | var stitchLocalPath = string.Join(Path.DirectorySeparatorChar.ToString(), |
||
323 | path.PathSplit(Path.DirectorySeparatorChar) |
||
324 | .Concat(file.Path)); |
||
325 | |||
326 | switch (file.PathType) |
||
327 | { |
||
328 | case StitchPathType.PATH_DIRECTORY: |
||
329 | // Create the directory. |
||
330 | if (!dryrun) |
||
331 | Directory.CreateDirectory(stitchLocalPath); |
||
332 | continue; |
||
333 | case StitchPathType.PATH_FILE: |
||
334 | // Create the directory to the stitch file. |
||
335 | if (!dryrun) |
||
336 | Directory.CreateDirectory( |
||
337 | string.Join(Path.DirectorySeparatorChar.ToString(), |
||
338 | stitchLocalPath.PathSplit(Path.DirectorySeparatorChar).Reverse() |
||
339 | .Skip(1) |
||
340 | .Reverse())); |
||
341 | break; |
||
342 | } |
||
343 | |||
344 | using (var memoryStream = new MemoryStream()) |
||
345 | { |
||
346 | using (var stream = client.Download(stitchRemotePath).Result) |
||
347 | { |
||
348 | stream.CopyTo(memoryStream); |
||
349 | } |
||
350 | memoryStream.Position = 0L; |
||
351 | if (!dryrun) |
||
352 | using (var fileStream = |
||
353 | IOExtensions.GetWriteStream(stitchLocalPath, FileMode.Create, |
||
354 | FileAccess.Write, FileShare.None, STITCH_CONSTANTS.LOCAL_FILE_ACCESS_TIMEOUT)) |
||
355 | { |
||
356 | memoryStream.CopyTo(fileStream); |
||
357 | } |
||
358 | } |
||
359 | } |
||
360 | catch (Exception ex) |
||
361 | { |
||
362 | throw new StitchException("Unable to stitch files.", ex); |
||
363 | } |
||
364 | |||
365 | // If no file patches was requested then do not patch and the process is complete. |
||
366 | if (nopatch) |
||
367 | return true; |
||
368 | |||
369 | StitchProgressUpdate("Patching files."); |
||
370 | |||
371 | // Retrive working file. |
||
372 | var workingFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), |
||
21 | office | 373 | path, STITCH_CONSTANTS.WORKING_CONFIGURATION_FILE); |
13 | office | 374 | |
375 | StitchProgressUpdate("Parsing working file to be patched."); |
||
376 | XDocument workingFile; |
||
377 | try |
||
378 | { |
||
379 | workingFile = XDocument.Load(workingFilePath); |
||
380 | } |
||
381 | catch (Exception ex) |
||
382 | { |
||
383 | throw new StitchException("Unable to parse working file to be patched.", ex); |
||
384 | } |
||
385 | |||
386 | // Retrieve default file. |
||
387 | StitchProgressUpdate("Parsing default file to be patched."); |
||
388 | XDocument defaultFile; |
||
389 | try |
||
390 | { |
||
391 | defaultFile = XDocument.Load(string.Join(Path.DirectorySeparatorChar.ToString(), |
||
21 | office | 392 | path, STITCH_CONSTANTS.DEFAULT_CONFIGURATION_FILE)); |
13 | office | 393 | } |
394 | catch (Exception ex) |
||
395 | { |
||
396 | throw new StitchException("Unable to parse default file to be patched.", ex); |
||
397 | } |
||
398 | |||
399 | // XPaths to exclude from patching. |
||
400 | var excludeXPaths = new HashSet<string>(); |
||
401 | if (stitchOptions.ConfigurationExcludes != null) |
||
402 | excludeXPaths.UnionWith(stitchOptions.ConfigurationExcludes.Tag); |
||
403 | |||
404 | // XPaths to force whilst patching. |
||
405 | var forceXPaths = new HashSet<string>(); |
||
406 | if (stitchOptions.ConfigurationForce != null) |
||
407 | forceXPaths.UnionWith(stitchOptions.ConfigurationForce.Tag); |
||
408 | |||
409 | // Patch the file. |
||
410 | StitchProgressUpdate("Patching file."); |
||
411 | var patchedFile = XML |
||
412 | .PatchXDocument(workingFile, defaultFile, forceXPaths, excludeXPaths); |
||
413 | if (patchedFile == null) |
||
414 | throw new StitchException("Unable to patch XML files."); |
||
415 | |||
416 | // Create a backup for the file to be patched. |
||
417 | StitchProgressUpdate("Creating a backup of the file to be patched."); |
||
418 | try |
||
419 | { |
||
420 | if (!dryrun) |
||
421 | File.Copy(workingFilePath, |
||
422 | string.Join(Path.DirectorySeparatorChar.ToString(), |
||
21 | office | 423 | path, STITCH_CONSTANTS.BACKUP_CONFIGURATION_FILE), true); |
13 | office | 424 | } |
425 | catch (Exception ex) |
||
426 | { |
||
427 | throw new StitchException("Unable to create patched file backup.", ex); |
||
428 | } |
||
429 | |||
430 | // Write the patched file. |
||
431 | StitchProgressUpdate("Saving the patched file."); |
||
432 | try |
||
433 | { |
||
434 | if (!dryrun) |
||
435 | patchedFile.Save(string.Join(Path.DirectorySeparatorChar.ToString(), |
||
21 | office | 436 | path, STITCH_CONSTANTS.WORKING_CONFIGURATION_FILE)); |
13 | office | 437 | } |
438 | catch (Exception ex) |
||
439 | { |
||
440 | throw new StitchException("Unable to save patched file.", ex); |
||
441 | } |
||
442 | |||
443 | StitchProgressUpdate("Stitching successful."); |
||
444 | return true; |
||
445 | } |
||
446 | } |
||
447 | } |