forked from Squirrel/Squirrel.Windows
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReleaseEntry.cs
More file actions
303 lines (247 loc) · 11.4 KB
/
ReleaseEntry.cs
File metadata and controls
303 lines (247 loc) · 11.4 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
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NuGet;
using Squirrel.SimpleSplat;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace Squirrel
{
public interface IReleaseEntry
{
string SHA1 { get; }
string Filename { get; }
long Filesize { get; }
bool IsDelta { get; }
string EntryAsString { get; }
SemanticVersion Version { get; }
string PackageName { get; }
float? StagingPercentage { get; }
string GetReleaseNotes(string packageDirectory);
Uri GetIconUrl(string packageDirectory);
}
[DataContract]
public class ReleaseEntry : IEnableLogger, IReleaseEntry
{
[DataMember] public string SHA1 { get; protected set; }
[DataMember] public string BaseUrl { get; protected set; }
[DataMember] public string Filename { get; protected set; }
[DataMember] public string Query { get; protected set; }
[DataMember] public long Filesize { get; protected set; }
[DataMember] public bool IsDelta { get; protected set; }
[DataMember] public float? StagingPercentage { get; protected set; }
protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta, string baseUrl = null, string query = null, float? stagingPercentage = null)
{
Contract.Requires(sha1 != null && sha1.Length == 40);
Contract.Requires(filename != null);
Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false);
Contract.Requires(filesize > 0);
SHA1 = sha1; BaseUrl = baseUrl; Filename = filename; Query = query; Filesize = filesize; IsDelta = isDelta; StagingPercentage = stagingPercentage;
}
[IgnoreDataMember]
public string EntryAsString {
get {
if (StagingPercentage != null) {
return String.Format("{0} {1}{2} {3} # {4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value));
} else {
return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize);
}
}
}
[IgnoreDataMember]
public SemanticVersion Version { get { return Filename.ToSemanticVersion(); } }
static readonly Regex packageNameRegex = new Regex(@"^([\w-]+)-\d+\..+\.nupkg$");
[IgnoreDataMember]
public string PackageName {
get {
var match = packageNameRegex.Match(Filename);
return match.Success ?
match.Groups[1].Value :
Filename.Substring(0, Filename.IndexOfAny(new[] { '-', '.' }));
}
}
public string GetReleaseNotes(string packageDirectory)
{
var zp = new ZipPackage(Path.Combine(packageDirectory, Filename));
var t = zp.Id;
if (String.IsNullOrWhiteSpace(zp.ReleaseNotes)) {
throw new Exception(String.Format("Invalid 'ReleaseNotes' value in nuspec file at '{0}'", Path.Combine(packageDirectory, Filename)));
}
return zp.ReleaseNotes;
}
public Uri GetIconUrl(string packageDirectory)
{
var zp = new ZipPackage(Path.Combine(packageDirectory, Filename));
return zp.IconUrl;
}
static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(.+)\s+(\d+)[\r]*$");
static readonly Regex commentRegex = new Regex(@"\s*#.*$");
static readonly Regex stagingRegex = new Regex(@"#\s+(\d{1,3})%$");
public static ReleaseEntry ParseReleaseEntry(string entry)
{
Contract.Requires(entry != null);
float? stagingPercentage = null;
var m = stagingRegex.Match(entry);
if (m != null && m.Success) {
stagingPercentage = Single.Parse(m.Groups[1].Value) / 100.0f;
}
entry = commentRegex.Replace(entry, "");
if (String.IsNullOrWhiteSpace(entry)) {
return null;
}
m = entryRegex.Match(entry);
if (!m.Success) {
throw new Exception("Invalid release entry: " + entry);
}
if (m.Groups.Count != 4) {
throw new Exception("Invalid release entry: " + entry);
}
string filename = m.Groups[2].Value;
// Split the base URL and the filename if an URI is provided,
// throws if a path is provided
string baseUrl = null;
string query = null;
if(Utility.IsHttpUrl(filename)) {
var uri = new Uri(filename);
var path = uri.LocalPath;
var authority = uri.GetLeftPart(UriPartial.Authority);
if (String.IsNullOrEmpty(path) || String.IsNullOrEmpty(authority)) {
throw new Exception("Invalid URL");
}
var indexOfLastPathSeparator = path.LastIndexOf("/") + 1;
baseUrl = authority + path.Substring(0, indexOfLastPathSeparator);
filename = path.Substring(indexOfLastPathSeparator);
if (!String.IsNullOrEmpty(uri.Query)) {
query = uri.Query;
}
}
if (filename.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) {
throw new Exception("Filename can either be an absolute HTTP[s] URL, *or* a file name");
}
long size = Int64.Parse(m.Groups[3].Value);
bool isDelta = filenameIsDeltaFile(filename);
return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query, stagingPercentage);
}
public bool IsStagingMatch(Guid? userId)
{
// A "Staging match" is when a user falls into the affirmative
// bucket - i.e. if the staging is at 10%, this user is the one out
// of ten case.
if (!StagingPercentage.HasValue) return true;
if (!userId.HasValue) return false;
uint val = BitConverter.ToUInt32(userId.Value.ToByteArray(), 12);
double percentage = ((double)val / (double)UInt32.MaxValue);
return percentage < StagingPercentage.Value;
}
public static IEnumerable<ReleaseEntry> ParseReleaseFile(string fileContents)
{
if (String.IsNullOrEmpty(fileContents)) {
return new ReleaseEntry[0];
}
fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents);
var ret = fileContents.Split('\n')
.Where(x => !String.IsNullOrWhiteSpace(x))
.Select(ParseReleaseEntry)
.Where(x => x != null)
.ToArray();
return ret.Any(x => x == null) ? null : ret;
}
public static IEnumerable<ReleaseEntry> ParseReleaseFileAndApplyStaging(string fileContents, Guid? userToken)
{
if (String.IsNullOrEmpty(fileContents)) {
return new ReleaseEntry[0];
}
fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents);
var ret = fileContents.Split('\n')
.Where(x => !String.IsNullOrWhiteSpace(x))
.Select(ParseReleaseEntry)
.Where(x => x != null && x.IsStagingMatch(userToken))
.ToArray();
return ret.Any(x => x == null) ? null : ret;
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, Stream stream)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(stream != null);
using (var sw = new StreamWriter(stream, Encoding.UTF8)) {
sw.Write(String.Join("\n", releaseEntries
.OrderBy(x => x.Version)
.ThenByDescending(x => x.IsDelta)
.Select(x => x.EntryAsString)));
}
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, string path)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(!String.IsNullOrEmpty(path));
using (var f = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) {
WriteReleaseFile(releaseEntries, f);
}
}
public static ReleaseEntry GenerateFromFile(Stream file, string filename, string baseUrl = null)
{
Contract.Requires(file != null && file.CanRead);
Contract.Requires(!String.IsNullOrEmpty(filename));
var hash = Utility.CalculateStreamSHA1(file);
return new ReleaseEntry(hash, filename, file.Length, filenameIsDeltaFile(filename), baseUrl);
}
public static ReleaseEntry GenerateFromFile(string path, string baseUrl = null)
{
using (var inf = File.OpenRead(path)) {
return GenerateFromFile(inf, Path.GetFileName(path), baseUrl);
}
}
public static List<ReleaseEntry> BuildReleasesFile(string releasePackagesDir)
{
var packagesDir = new DirectoryInfo(releasePackagesDir);
// Generate release entries for all of the local packages
var entriesQueue = new ConcurrentQueue<ReleaseEntry>();
Parallel.ForEach(packagesDir.GetFiles("*.nupkg"), x => {
using (var file = x.OpenRead()) {
entriesQueue.Enqueue(GenerateFromFile(file, x.Name));
}
});
// Write the new RELEASES file to a temp file then move it into
// place
var entries = entriesQueue.ToList();
var tempFile = default(string);
Utility.WithTempFile(out tempFile, releasePackagesDir);
try {
using (var of = File.OpenWrite(tempFile)) {
if (entries.Count > 0) WriteReleaseFile(entries, of);
}
var target = Path.Combine(packagesDir.FullName, "RELEASES");
if (File.Exists(target)) {
File.Delete(target);
}
File.Move(tempFile, target);
} finally {
if (File.Exists(tempFile)) Utility.DeleteFileHarder(tempFile, true);
}
return entries;
}
static string stagingPercentageAsString(float percentage)
{
return String.Format("{0:F0}%", percentage * 100.0);
}
static bool filenameIsDeltaFile(string filename)
{
return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase);
}
public static ReleasePackage GetPreviousRelease(IEnumerable<ReleaseEntry> releaseEntries, IReleasePackage package, string targetDir)
{
if (releaseEntries == null || !releaseEntries.Any()) return null;
return releaseEntries
.Where(x => x.IsDelta == false)
.Where(x => x.Version < package.ToSemanticVersion())
.OrderByDescending(x => x.Version)
.Select(x => new ReleasePackage(Path.Combine(targetDir, x.Filename), true))
.FirstOrDefault();
}
}
}