-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathGithubUpdateCheck.cs
More file actions
631 lines (542 loc) · 25.3 KB
/
Copy pathGithubUpdateCheck.cs
File metadata and controls
631 lines (542 loc) · 25.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
using System;
using System.Globalization;
using System.Net;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Mayerch1.GithubUpdateCheck
{
/// <summary>
/// Enumeration of version steps (Major, Minor,...)
/// Value equals to 1-based index of the version.
/// May be replaced by int, casted to <see cref="VersionChange"/>
/// </summary>
public enum VersionChange
{
/// <summary>
/// Major software update (X.0.0.0)
/// </summary>
Major = 1,
/// <summary>
/// Minor software update (0.X.0.0)
/// </summary>
Minor = 2,
/// <summary>
/// Build change (0.0.X.0)
/// </summary>
Build = 3,
/// <summary>
/// Revision changed (0.0.0.X)
/// </summary>
Revision = 4
}
/// <summary>
/// Specifies the algorithm for comparing the local and the remote version number
/// </summary>
public enum CompareType
{
/// <summary>
/// Compares versions of the pattern 1.2.3.4 (e.g. 2.0.0 is higher than 1.9999.0)
/// </summary>
Incremental = 0,
/// <summary>
/// If the remote string is different, it will assume an update
/// </summary>
Boolean = 1
}
/// <summary>
/// The exception that is thrown if the version argument does not match the required pattern
/// </summary>
public class InvalidVersionException : ArgumentException {
/// <summary>
/// Initializes a new instance of the <see cref="InvalidVersionException"/> class
/// </summary>
[ExcludeFromCodeCoverage]
public InvalidVersionException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidVersionException"/> class with a specified error message
/// </summary>
public InvalidVersionException(String message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="InvalidVersionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception
/// </summary>
/// <param name="message"></param>
/// <param name="innerException"></param>
[ExcludeFromCodeCoverage]
public InvalidVersionException(string message, Exception innerException) : base(message, innerException)
{
}
};
/// <summary>
/// The exception is thrown if the remote repo does not have releases
/// </summary>
public class NoVersionException: InvalidVersionException
{
/// <summary>
/// Initializes a new instance of the <see cref="NoVersionException"/> class
/// </summary>
[ExcludeFromCodeCoverage]
public NoVersionException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NoVersionException"/> class with a specified error message
/// </summary>
public NoVersionException(String message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="NoVersionException"/> class with a specified error message and a reference to the inner exception that is the cause of this exception
/// </summary>
/// <param name="message"></param>
/// <param name="innerException"></param>
[ExcludeFromCodeCoverage]
public NoVersionException(string message, Exception innerException) : base(message, innerException)
{
}
}
/// <summary>
/// Checks if your github repo is on a newer version or not
/// </summary>
#pragma warning disable CA1724 // Do not warn about namespace/classname conflict
public class GithubUpdateCheck : IEquatable<GithubUpdateCheck>
{
private const string githubUrl = "https://github.com/";
private const string latestVersionString = "/releases/latest";
private readonly string Username;
private readonly string Repository;
private readonly CompareType cmpType;
/// <summary>
/// Get the set compare type. (Getter only)
/// </summary>
public CompareType CompareType
{
get => cmpType;
}
/// <summary>
/// Assumes version numbering with the pattern 1.2.3.4
/// </summary>
/// <param name="Username">Username of Repository owner</param>
/// <param name="Repository">Name of Github Repository</param>
public GithubUpdateCheck(string Username, string Repository)
{
this.Username = Username;
this.Repository = Repository;
cmpType = CompareType.Incremental;
}
/// <summary>
/// Choose the compare system based on <see cref="CompareType"/>
/// </summary>
/// <param name="Username">Username of Repository owner</param>
/// <param name="Repository">Name of Github Repository</param>
/// <param name="compareType">Method to compare the local with the remote version</param>
public GithubUpdateCheck(string Username, string Repository, CompareType compareType)
{
this.Username = Username;
this.Repository = Repository;
cmpType = compareType;
}
/// <summary>
/// Compare two instances. If all (private) members are equal, the object is considered equal
/// </summary>
/// <param name="a">first instance</param>
/// <param name="b">second instance</param>
/// <returns>true if all members are equal</returns>
public static bool operator ==(GithubUpdateCheck a, GithubUpdateCheck b)
{
// check both sides for null
if ((b is null) || (a is null))
{
// if both are null, return true
if ((a is null) && (b is null))
{
return true;
}
else
{
return false;
}
}
// all members must be the same
return (a.Username == b.Username) && (a.Repository == b.Repository) && (a.cmpType == b.cmpType);
}
/// <summary>
/// Compare two instances. If at least one (private) members is different, the object is considered not-equal
/// </summary>
/// <param name="a">first instance</param>
/// <param name="b">second instance</param>
/// <returns>true if at least one member is different</returns>
public static bool operator !=(GithubUpdateCheck a, GithubUpdateCheck b)
{
return !(a == b);
}
/// <summary>
/// Compare two instances. If all (private) members are equal, the object is considered equal
/// </summary>
/// <param name="obj">other object</param>
/// <returns>False if objects are not the same type. Otherwise behaves like ==</returns>
[ExcludeFromCodeCoverage]
public override bool Equals(object obj)
{
return Equals(obj as GithubUpdateCheck);
}
/// <summary>
/// Compare two instances. If all (private) members are equal, the object is considered equal
/// </summary>
/// <param name="other">the other object</param>
/// <returns>true - if all members are equal</returns>
public bool Equals(GithubUpdateCheck other)
{
return other != null &&
Username == other.Username &&
Repository == other.Repository &&
cmpType == other.cmpType;
}
/// <summary>
/// Get the hash code of the object (generated by VS refactoring)
/// </summary>
/// <returns></returns>
[ExcludeFromCodeCoverage]
public override int GetHashCode()
{
var hashCode = -1134592763;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Username);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Repository);
hashCode = hashCode * -1521134295 + cmpType.GetHashCode();
return hashCode;
}
/// <summary>
/// Tests if inputted version is valid based on the allowed/specified patterns
/// </summary>
/// <param name="version">Current software version, is compared against the github version</param>
/// <returns></returns>
private bool IsValidInput(string version)
{
switch (cmpType)
{
case CompareType.Incremental:
return IsValidInputIncremental(version);
// boolean is fallback, because it accepts the widest range of input
case CompareType.Boolean:
default:
return IsValidInputBoolean(version);
}
}
/// <summary>
/// Tests if inputted version is valid based on the allowed/specified patterns
/// </summary>
/// <param name="version">Current software version, is compared against the github version</param>
/// <returns></returns>
private static bool IsValidInputIncremental(string version)
{
if (version != null)
{
// 1.0.0.0
// 1.0.0
// v.1.0.0
// v1.0.0
// and any combination of those
string pattern = @"^([^0-9]\.?)?(\d+\.)*\d+$";
Match match = Regex.Match(version, pattern);
return match.Success;
}
else
{
return false;
}
}
/// <summary>
/// Tests if inputted version is not null
/// </summary>
/// <param name="version">Current software version, is compared against the github version</param>
/// <returns></returns>
private static bool IsValidInputBoolean(string version)
{
return (version != null);
}
/// <summary>
/// strip the given url, to only contain the github version (not normalized)
/// </summary>
/// <param name="url"></param>
/// <returns>striped repo version string</returns>
private string ExtractVersionFromUrl(string url)
{
// extract the tag from the url and validate/normalize input
//no releases yet
if (!url.Contains("/tag/"))
throw new NoVersionException(url + " the github version number does not contain \"tag\" in its path. Usually occurs when no release exists. [Remote error]");
//get everything after last /tag/
int indexOfTag = CultureInfo.InvariantCulture.CompareInfo.LastIndexOf(url, "/tag/");
url = url.Substring(indexOfTag + "/tag/".Length);
if (!IsValidInput(url))
{
throw new InvalidVersionException(url + " the github version number does not follow the specified version pattern [Remote error]");
}
return url;
}
/// <summary>
/// Removes leading v. and v from the string
/// </summary>
/// <param name="version">string must pass <see cref="IsValidInput(string)"/></param>
/// <returns>normalized string</returns>
private string NormalizeVersionString(string version)
{
switch (cmpType)
{
case CompareType.Incremental:
return NormalizeVersionStringIncremental(version);
// boolean is fallback, because it accepts the widest range of input
case CompareType.Boolean:
default:
return NormalizeVersionStringBoolean(version);
}
}
/// <summary>
/// Returns the latest version on github.
/// </summary>
/// <exception cref="InvalidVersionException">Is thrown if the supplied version or the remote version does not match the allowed version pattern</exception>
/// <exception cref="NoVersionException">Is thrown if the remote repo does not have any releases</exception>
/// <returns>normalized string</returns>
public async Task<string> VersionAsync()
{
string resolved = await GetResponseUrlAsync(githubUrl + Username + "/" + Repository + latestVersionString).ConfigureAwait(false);
return extractVersion(resolved);
}
/// <summary>
/// Returns the latest version on github.
/// </summary>
/// <exception cref="InvalidVersionException">Is thrown if the supplied version or the remote version does not match the allowed version pattern</exception>
/// <exception cref="NoVersionException">Is thrown if the remote repo does not have any releases</exception>
/// <returns>normalized string</returns>
public string Version()
{
string resolved = GetResponseUrl(githubUrl + Username + "/" + Repository + latestVersionString);
return extractVersion(resolved);
}
/// <summary>
/// extract the normalized version from the given url, may throw <see cref="NoVersionException"/>
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private string extractVersion(string url)
{
string extract = ExtractVersionFromUrl(url); // strip the url part
string version = NormalizeVersionString(extract);
return version;
}
/// <summary>
/// Removes leading v. and v from the string
/// </summary>
/// <param name="version">string which is compliant to the allowed pattern(s)</param>
/// <returns>normalized string</returns>
private static string NormalizeVersionStringIncremental(string version)
{
// leading (v. or v ) are allowed
// therefore remove those from the version number
string pattern = @"(\d+\.)*\d+$";
Match match = Regex.Match(version, pattern);
return match.Value;
}
/// <summary>
/// returns version, only to keep consistency
/// </summary>
/// <param name="version">string which is compliant to the allowed pattern(s)</param>
/// <returns>normalized string</returns>
private static string NormalizeVersionStringBoolean(string version)
{
// the boolean compare takes the version as is
return version;
}
/// <summary>
/// <para>Compares the current software version to the latest release on github. Asynchronous web request</para>
/// If the webservice is not available this function will assume no updates available
/// </summary>
/// <param name="CurrentVersion">The version of the software wich is compared to the github version</param>
/// <param name="VersionChange">The granularity of the comparison. Any version change smaller than this will be ignored. (e.g. Minor will check for a change in the first 2 digits groups). Does not apply to <see cref="CompareType.Boolean"/></param>
/// <exception cref="InvalidVersionException">Is thrown if the supplied version or the remote version does not match the allowed version pattern</exception>
/// <returns>bool - true if a newer version is available, false - if no newer version is available or if no connection to github is available</returns>
public async Task<bool> IsUpdateAvailableAsync(string CurrentVersion, VersionChange VersionChange = VersionChange.Minor)
{
if (!IsValidInput(CurrentVersion))
{
throw new InvalidVersionException(CurrentVersion + " does not follow the specified version pattern [CurrentVersion]");
}
string resolved = await GetResponseUrlAsync(githubUrl + Username + "/" + Repository + latestVersionString).ConfigureAwait(false);
if (resolved != null)
return CompareVersions(NormalizeVersionString(CurrentVersion), resolved, VersionChange);
else
return false;
}
/// <summary>
/// <para>Compares the current software version to the latest release on github. Synchronous (blocking) web request</para>
/// If the webservice is not available this function will assume no updates available
/// </summary>
/// <param name="CurrentVersion">The version of the software wich is compared to the github version</param>
/// <param name="VersionChange">The granularity of the comparison. Any version change smaller than this will be ignored. (e.g. Minor will check for a change in the first 2 digits groups). Does not apply to <see cref="CompareType.Boolean"/></param>
/// <exception cref="InvalidVersionException">Is thrown if the supplied version or the remote version does not match the allowed version pattern</exception>
/// <returns>bool - true if a newer version is available, false - if no newer version is available or if no connection to github is available</returns>
public bool IsUpdateAvailable(string CurrentVersion, VersionChange VersionChange = VersionChange.Minor)
{
if (!IsValidInput(CurrentVersion))
{
throw new InvalidVersionException(CurrentVersion + " does not follow the specified version pattern [CurrentVersion]");
}
string resolved = GetResponseUrl(githubUrl + Username + "/" + Repository + latestVersionString);
if (resolved != null)
return CompareVersions(NormalizeVersionString(CurrentVersion), resolved, VersionChange);
else
return false;
}
/// <summary>
/// Compares two version numbers. Extract version number of github release url
/// "Current" must comply with pattern, aswell as github version
/// Respects the selected compareType
/// </summary>
/// <param name="current">Local software version, must be checket for complinance with the allowed pattern(s)</param>
/// <param name="github">Url of the latest github release</param>
/// <param name="changeLevel">The level for comparison. Does not apply to Boolean compare</param>
/// /// <exception cref="InvalidVersionException">Is thrown if the supplied version does not match the allowed version pattern</exception>
/// <returns>true if a newer version is available</returns>
private bool CompareVersions(string current, string github, VersionChange changeLevel)
{
try
{
github = extractVersion(github);
}
catch (NoVersionException)
{
// assume this is equivalent to no update
return false;
}
// choose CompareType specific compare method
switch (cmpType)
{
case CompareType.Incremental:
return CompareVersionsIncremental(current, github, changeLevel);
// boolean is fallback, because it accepts the widest range of input
case CompareType.Boolean:
default:
return CompareVersionsBoolean(current, github);
}
}
/// <summary>
/// Compares two version numbers. Extract version number of github release url
/// Assumes update is available, if remote version is different to "current"
/// </summary>
/// <param name="current">Local software version</param>
/// <param name="github">extracted, validated and normalized version string of the remote repo</param>
/// /// <exception cref="InvalidVersionException">Is thrown if the supplied version does not match the allowed version pattern</exception>
/// <returns></returns>
private static bool CompareVersionsBoolean(string current, string github)
{
// if they are different, an update is available
return current != github;
}
/// <summary>
/// Compares two version numbers. Extract version number of github release url
/// "Current" must comply with pattern, aswell as github version
/// </summary>
/// <param name="current">Local software version, must be checket for complinance with the allowed pattern(s)</param>
/// <param name="github">extracted, validated and normalized version string of the remote repo</param>
/// <param name="changeLevel">The level for comparison</param>
/// /// <exception cref="InvalidVersionException">Is thrown if the supplied version does not match the allowed version pattern</exception>
/// <returns></returns>
private bool CompareVersionsIncremental(string current, string github, VersionChange changeLevel)
{
// input is tested for numbers only between the seperators ('.')
Int64[] currentArr = Array.ConvertAll(current.Split('.'), s => Int64.Parse(s, NumberStyles.Integer, CultureInfo.InvariantCulture));
Int64[] gitArr = Array.ConvertAll(github.Split('.'), s => Int64.Parse(s, NumberStyles.Integer, CultureInfo.InvariantCulture));
// set the comparison depth
// take the minimum depth, in case one specified number is smaller than another one
int cmpDepth = (int)changeLevel;
cmpDepth = Math.Min(currentArr.Length, cmpDepth);
cmpDepth = Math.Min(gitArr.Length, cmpDepth);
/* comparison of version numbers as follows
localMajor <> gitMajor
[smaller] [equals] [greater]
true | false
V
localMinor <> gitMinor
[smaller] [equals] [greater]
true | false
V
.
.
.
// if cmpDepth is reached, the return is false
*/
// implicitie cmpDepth == 1
// 1. major is checked on every version
// 2. input is guaranteed to have cmpDepth >= 1
return CompareVersionIncrementalRecursive(cmpDepth, currentArr, gitArr);
}
/// <summary>
/// Compare one element of the version array with the same element of the remoteVersion. Increments index on every call.
/// Aborts if maxDepth is reached.
/// maxDepth must not exceed the length of localVersion nor the remoteVersion
/// </summary>
/// <param name="maxDepth">Maximum compare depth, must be greater 0. Must not exceed localVersion.Length nor remoteVersion.Length, (x.Length is the last allowed value and will stop the recursion)</param>
/// <param name="currentDepth">the current index of the comparison, is incremented in every call. Recursion starts with 0</param>
/// <param name="localVersion">int array of the local version</param>
/// <param name="remoteVersion">int array of the remote array</param>
/// <returns></returns>
private bool CompareVersionIncrementalRecursive(int maxDepth, Int64[] localVersion, Int64[] remoteVersion, int currentDepth=0)
{
if (currentDepth == maxDepth)
{
return false;
}
else if(localVersion[currentDepth] > remoteVersion[currentDepth]){
return false;
}
else if (localVersion[currentDepth] < remoteVersion[currentDepth]){
return true;
}
else
{
return CompareVersionIncrementalRecursive(maxDepth, localVersion, remoteVersion, currentDepth + 1);
}
}
private static string GetResponseUrl(string request)
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(request);
WebResponse wResp;
try
{
wResp = req.GetResponse();
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
//if not available
return null;
}
return wResp.ResponseUri.ToString();
}
private static async Task<string> GetResponseUrlAsync(string request)
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(request);
WebResponse wResp;
try
{
// this is not for ui and does not necessarily need to return to the main thread
wResp = await req.GetResponseAsync().ConfigureAwait(false);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
return null;
//if not available
}
return wResp.ResponseUri.ToString();
}
}
#pragma warning restore CA1724 // Do not warn about namespace/classname conflict
}