markus / MarkusAutoUpdate / src / NetSparkle.Tools.AppCastGenerator / Program.cs @ cc749215
이력 | 보기 | 이력해설 | 다운로드 (15.8 KB)
1 |
using NetSparkleUpdater; |
---|---|
2 |
using System; |
3 |
using System.Diagnostics; |
4 |
using System.IO; |
5 |
using System.Collections.Generic; |
6 |
using System.Xml; |
7 |
using NetSparkleUpdater.AppCastHandlers; |
8 |
using System.Text; |
9 |
using CommandLine; |
10 |
using System.Text.RegularExpressions; |
11 |
using System.Linq; |
12 |
using Console = Colorful.Console; |
13 |
using System.Drawing; |
14 |
using NetSparkleUpdater.AppCastGenerator; |
15 |
using System.Web; |
16 |
using System.Net.Http.Headers; |
17 |
using System.Threading; |
18 |
using System.Net; |
19 |
using System.IO.Compression; |
20 |
|
21 |
namespace NetSparkleUpdater.Tools.AppCastGenerator |
22 |
{ |
23 |
internal class Program |
24 |
{ |
25 |
public class Options |
26 |
{ |
27 |
[Option('a', "appcast-output-directory", Required = false, HelpText = "Directory to write appcast.xml")] |
28 |
public string OutputDirectory { get; set; } |
29 |
|
30 |
[Option('e', "ext", SetName = "local", Required = false, HelpText = "Search for file extensions.", Default = "exe")] |
31 |
public string Extension { get; set; } |
32 |
|
33 |
[Option('b', "binaries", SetName = "local", Required = false, HelpText = "Directory containing binaries.", Default = ".")] |
34 |
public string SourceBinaryDirectory { get; set; } |
35 |
|
36 |
//[Option('g', "github-atom-feed", SetName = "github", Required = false, HelpText = "Generate from Github release atom feed (signatures not supported yet)")] |
37 |
//public string GithubAtomFeed { get; set; } |
38 |
|
39 |
[Option('f', "file-extract-version", SetName = "local", Required = false, HelpText = "Determine the version from the file name", Default = false)] |
40 |
public bool FileExtractVersion { get; set; } |
41 |
|
42 |
[Option('o', "os", Required = false, HelpText = "Operating System (windows,macos,linux)", Default = "windows")] |
43 |
public string OperatingSystem { get; set; } |
44 |
|
45 |
[Option('u', "base-url", SetName = "local", Required = false, HelpText = "Base URL for downloads", Default = null)] |
46 |
public Uri BaseUrl { get; set; } |
47 |
|
48 |
[Option('l', "change-log-url", SetName = "local", Required = false, HelpText = "Base URL to the location for your changelog files on some server for downloading", Default = "")] |
49 |
public string ChangeLogUrl { get; set; } |
50 |
|
51 |
[Option('p', "change-log-path", SetName = "local", Required = false, HelpText = "File path to Markdown changelog files (expected extension: .md; version must match AssemblyVersion, e.g. MyApp 1.0.0.md).", Default = "")] |
52 |
public string ChangeLogPath { get; set; } |
53 |
|
54 |
[Option('n', "product-name", Required = false, HelpText = "Product Name", Default = "Application")] |
55 |
public string ProductName { get; set; } |
56 |
|
57 |
[Option('x', "url-prefix-version", SetName = "local", Required = false, HelpText = "Add the version as a prefix to the download url")] |
58 |
public bool PrefixVersion { get; set; } |
59 |
|
60 |
[Option('v', "Fixed version", SetName = "local", Required = false, HelpText = "Fixed Version (All files) Option",Default =null)] |
61 |
public string FixedVersion { get; set; } |
62 |
|
63 |
[Option("key-path", SetName = "local", Required = false, HelpText = "Path to NetSparkle_Ed25519.priv and NetSparkle_Ed25519.pub files")] |
64 |
public string PathToKeyFiles { get; set; } |
65 |
|
66 |
|
67 |
#region Key Generation |
68 |
|
69 |
[Option("generate-keys", SetName = "keys", Required = false, HelpText = "Generate keys")] |
70 |
public bool GenerateKeys { get; set; } |
71 |
|
72 |
[Option("force", SetName = "keys", Required = false, HelpText = "Force regeneration of keys")] |
73 |
public bool ForceRegeneration { get; set; } |
74 |
|
75 |
[Option("export", SetName = "keys", Required = false, HelpText = "Export keys")] |
76 |
public bool Export { get; set; } |
77 |
|
78 |
#endregion |
79 |
|
80 |
|
81 |
#region Getting Signatures for Binaries |
82 |
|
83 |
[Option("generate-signature", SetName = "signing", Required = false, HelpText = "Generate signature from binary")] |
84 |
public string BinaryToSign { get; set; } |
85 |
|
86 |
#endregion |
87 |
|
88 |
#region Verifying Binary Signatures |
89 |
|
90 |
[Option("verify", SetName = "verify", Required = false, HelpText = "Binary to verify")] |
91 |
public string BinaryToVerify { get; set; } |
92 |
|
93 |
[Option("signature", SetName = "verify", Required = false, HelpText = "Signature")] |
94 |
public string Signature { get; set; } |
95 |
|
96 |
#endregion |
97 |
|
98 |
} |
99 |
|
100 |
|
101 |
private static string[] _operatingSystems = new string[] { "windows", "mac", "linux" }; |
102 |
private static SignatureManager _signatureManager = new SignatureManager(); |
103 |
|
104 |
static void Main(string[] args) |
105 |
{ |
106 |
Parser.Default.ParseArguments<Options>(args) |
107 |
.WithParsed(Run) |
108 |
.WithNotParsed(HandleParseError); |
109 |
} |
110 |
static void Run(Options opts) |
111 |
{ |
112 |
/* |
113 |
if (opts.GithubAtomFeed != null) |
114 |
{ |
115 |
GenerateFromAtom(opts); |
116 |
return; |
117 |
}*/ |
118 |
|
119 |
if (!string.IsNullOrWhiteSpace(opts.PathToKeyFiles)) |
120 |
{ |
121 |
_signatureManager.SetStorageDirectory(opts.PathToKeyFiles); |
122 |
} |
123 |
|
124 |
if (opts.Export) |
125 |
{ |
126 |
Console.WriteLine("Private Key:"); |
127 |
Console.WriteLine(Convert.ToBase64String(_signatureManager.GetPrivateKey())); |
128 |
Console.WriteLine("Public Key:"); |
129 |
Console.WriteLine(Convert.ToBase64String(_signatureManager.GetPublicKey())); |
130 |
return; |
131 |
} |
132 |
|
133 |
if (opts.GenerateKeys) |
134 |
{ |
135 |
var didSucceed = _signatureManager.Generate(opts.ForceRegeneration); |
136 |
if (didSucceed) |
137 |
{ |
138 |
Console.WriteLine("Keys successfully generated", Color.Green); |
139 |
} |
140 |
else |
141 |
{ |
142 |
Console.WriteLine("Keys failed to generate", Color.Red); |
143 |
} |
144 |
return; |
145 |
} |
146 |
|
147 |
if (opts.BinaryToSign != null) |
148 |
{ |
149 |
var signature = _signatureManager.GetSignatureForFile(new FileInfo(opts.BinaryToSign)); |
150 |
|
151 |
Console.WriteLine($"Signature: {signature}", Color.Green); |
152 |
|
153 |
return; |
154 |
} |
155 |
|
156 |
if (opts.BinaryToVerify != null) |
157 |
{ |
158 |
var result = _signatureManager.VerifySignature(new FileInfo(opts.BinaryToVerify), opts.Signature); |
159 |
|
160 |
if (result) |
161 |
{ |
162 |
Console.WriteLine($"Signature valid", Color.Green); |
163 |
} |
164 |
else |
165 |
{ |
166 |
Console.WriteLine($"Signature invalid", Color.Red); |
167 |
} |
168 |
|
169 |
return; |
170 |
} |
171 |
|
172 |
|
173 |
var search = $"*.{opts.Extension}"; |
174 |
|
175 |
if (opts.SourceBinaryDirectory == ".") |
176 |
{ |
177 |
opts.SourceBinaryDirectory = Environment.CurrentDirectory; |
178 |
} |
179 |
|
180 |
var binaries = Directory.GetFiles(opts.SourceBinaryDirectory, search,SearchOption.AllDirectories); |
181 |
|
182 |
if (binaries.Length == 0) |
183 |
{ |
184 |
Console.WriteLine($"No files founds matching {search} in {opts.SourceBinaryDirectory}", Color.Yellow); |
185 |
Environment.Exit(1); |
186 |
} |
187 |
|
188 |
if (!_operatingSystems.Any(opts.OperatingSystem.Contains)) |
189 |
{ |
190 |
Console.WriteLine($"Invalid operating system: {opts.OperatingSystem}", Color.Red); |
191 |
Console.WriteLine($"Valid options are : windows, macos or linux"); |
192 |
Environment.Exit(1); |
193 |
} |
194 |
|
195 |
if (string.IsNullOrEmpty(opts.OutputDirectory)) |
196 |
{ |
197 |
opts.OutputDirectory = opts.SourceBinaryDirectory; |
198 |
} |
199 |
|
200 |
Console.WriteLine(""); |
201 |
Console.WriteLine($"Operating System: {opts.OperatingSystem}", Color.Blue); |
202 |
Console.WriteLine($"Searching: {opts.SourceBinaryDirectory}", Color.Blue); |
203 |
Console.WriteLine($"Found {binaries.Count()} {opts.Extension} files(s)", Color.Blue); |
204 |
Console.WriteLine(""); |
205 |
|
206 |
try |
207 |
{ |
208 |
|
209 |
var productName = opts.ProductName; |
210 |
|
211 |
var items = new List<AppCastItem>(); |
212 |
|
213 |
var usesChangelogs = !string.IsNullOrWhiteSpace(opts.ChangeLogPath) && Directory.Exists(opts.ChangeLogPath); |
214 |
|
215 |
if(string.IsNullOrWhiteSpace(opts.FixedVersion)) |
216 |
{ |
217 |
Console.WriteLine("FixedVersion Null. setting -v x.x.x", Color.Red); |
218 |
Environment.Exit(1); |
219 |
} |
220 |
|
221 |
string tempfile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); |
222 |
|
223 |
CreateZip(opts.SourceBinaryDirectory, tempfile); |
224 |
|
225 |
var fileInfo = new FileInfo(tempfile); |
226 |
|
227 |
var fileVersion = FileVersion(fileInfo,opts.FixedVersion,opts.FileExtractVersion); |
228 |
|
229 |
var productVersion = fileVersion; |
230 |
var itemTitle = string.IsNullOrWhiteSpace(productName) ? productVersion : productName + " " + productVersion; |
231 |
|
232 |
var remoteUpdateFileName = $"{productName}_{opts.FixedVersion}.zip"; |
233 |
|
234 |
var remoteUpdateFilePath = $"{(opts.PrefixVersion ? $"{fileVersion}/" :"")}{HttpUtility.UrlEncode(remoteUpdateFileName)}"; |
235 |
|
236 |
Copy(tempfile, Path.Combine(opts.OutputDirectory, remoteUpdateFilePath.Replace('/', '\\'))); |
237 |
|
238 |
var remoteUpdateFile = $"{opts.BaseUrl}{remoteUpdateFilePath}"; |
239 |
|
240 |
// changelog stuff |
241 |
var changelogFileName = productVersion + ".md"; |
242 |
var changelogPath = Path.Combine(opts.ChangeLogPath, changelogFileName); |
243 |
var hasChangelogForFile = usesChangelogs && File.Exists(changelogPath); |
244 |
var changelogSignature = ""; |
245 |
|
246 |
if (hasChangelogForFile) |
247 |
{ |
248 |
changelogSignature = _signatureManager.GetSignatureForFile(changelogPath); |
249 |
} |
250 |
|
251 |
// |
252 |
var item = new AppCastItem() |
253 |
{ |
254 |
Title = itemTitle, |
255 |
DownloadLink = remoteUpdateFile, |
256 |
Version = productVersion, |
257 |
ShortVersion = productVersion.Substring(0, productVersion.LastIndexOf('.')), |
258 |
PublicationDate = fileInfo.CreationTime, |
259 |
UpdateSize = fileInfo.Length, |
260 |
Description = "", |
261 |
DownloadSignature = _signatureManager.KeysExist() ? _signatureManager.GetSignatureForFile(fileInfo) : null, |
262 |
OperatingSystemString = opts.OperatingSystem, |
263 |
MIMEType = MimeTypes.GetMimeType(fileInfo.Name) |
264 |
}; |
265 |
|
266 |
if (hasChangelogForFile) |
267 |
{ |
268 |
if (!string.IsNullOrWhiteSpace(opts.ChangeLogUrl)) |
269 |
{ |
270 |
item.ReleaseNotesSignature = changelogSignature; |
271 |
item.ReleaseNotesLink = opts.ChangeLogUrl + changelogFileName; |
272 |
} |
273 |
else |
274 |
{ |
275 |
item.Description = File.ReadAllText(changelogPath); |
276 |
} |
277 |
} |
278 |
|
279 |
items.Add(item); |
280 |
|
281 |
// appcast 생성 폴더에 업데이트가 필요한 파일을 복사 |
282 |
|
283 |
|
284 |
|
285 |
var appcastXmlDocument = XMLAppCast.GenerateAppCastXml(items, productName); |
286 |
|
287 |
var appcastFileName = Path.Combine(opts.OutputDirectory, "appcast.xml"); |
288 |
|
289 |
var dirName = Path.GetDirectoryName(appcastFileName); |
290 |
|
291 |
if (!Directory.Exists(dirName)) |
292 |
{ |
293 |
Console.WriteLine("Creating {0}", dirName); |
294 |
Directory.CreateDirectory(dirName); |
295 |
} |
296 |
else |
297 |
{ |
298 |
if(File.Exists(appcastFileName)) |
299 |
{ |
300 |
File.Copy(appcastFileName, $"{ appcastFileName}{ opts.FixedVersion}", true); |
301 |
} |
302 |
} |
303 |
|
304 |
Console.WriteLine("Writing appcast to {0}", appcastFileName); |
305 |
|
306 |
using (var w = XmlWriter.Create(appcastFileName, new XmlWriterSettings { NewLineChars = "\n", Encoding = new UTF8Encoding(false) })) |
307 |
{ |
308 |
appcastXmlDocument.Save(w); |
309 |
} |
310 |
|
311 |
if (_signatureManager.KeysExist()) |
312 |
{ |
313 |
var appcastFile = new FileInfo(appcastFileName); |
314 |
var signatureFile = appcastFileName + ".signature"; |
315 |
|
316 |
if (File.Exists(signatureFile)) |
317 |
{ |
318 |
File.Copy(signatureFile, $"{ signatureFile}{ opts.FixedVersion}",true); |
319 |
} |
320 |
|
321 |
var signature = _signatureManager.GetSignatureForFile(appcastFile); |
322 |
|
323 |
var result = _signatureManager.VerifySignature(appcastFile, signature); |
324 |
|
325 |
if (result) |
326 |
{ |
327 |
|
328 |
File.WriteAllText(signatureFile, signature); |
329 |
Console.WriteLine($"Wrote {signatureFile}", Color.Green); |
330 |
} |
331 |
else |
332 |
{ |
333 |
Console.WriteLine($"Failed to verify {signatureFile}", Color.Red); |
334 |
} |
335 |
} |
336 |
else |
337 |
{ |
338 |
Console.WriteLine("Skipped generating signature. Generate keys with --generate-keys", Color.Red); |
339 |
Environment.Exit(1); |
340 |
} |
341 |
} |
342 |
catch (Exception e) |
343 |
{ |
344 |
Console.WriteLine(e.Message); |
345 |
Console.WriteLine(); |
346 |
Environment.Exit(1); |
347 |
} |
348 |
} |
349 |
|
350 |
private static string[] CreateZip(string sourceBinaryDirectory,string targetFile) |
351 |
{ |
352 |
ZipFile.CreateFromDirectory(sourceBinaryDirectory, targetFile); |
353 |
|
354 |
return new[] { targetFile }; |
355 |
} |
356 |
|
357 |
private static string FileVersion(FileInfo fileInfo,string fixedVersion,bool fileExtractVersion) |
358 |
{ |
359 |
string version = ""; |
360 |
|
361 |
if (!string.IsNullOrWhiteSpace(fixedVersion)) |
362 |
{ |
363 |
version = fixedVersion; |
364 |
} |
365 |
else |
366 |
{ |
367 |
if (fileExtractVersion) |
368 |
{ |
369 |
version = GetVersionFromName(fileInfo); |
370 |
} |
371 |
else |
372 |
{ |
373 |
version = GetVersionFromAssembly(fileInfo); |
374 |
|
375 |
} |
376 |
} |
377 |
|
378 |
return version; |
379 |
} |
380 |
|
381 |
|
382 |
static private void Copy(string sourceFileName, string destFileName) |
383 |
{ |
384 |
try |
385 |
{ |
386 |
var dirName = Path.GetDirectoryName(destFileName); |
387 |
|
388 |
if (!Directory.Exists(dirName)) |
389 |
{ |
390 |
Console.WriteLine("Creating {0}", dirName); |
391 |
Directory.CreateDirectory(dirName); |
392 |
} |
393 |
|
394 |
File.Copy(sourceFileName, destFileName,true); |
395 |
} |
396 |
catch (Exception e) |
397 |
{ |
398 |
Console.WriteLine(e.Message); |
399 |
Console.WriteLine(); |
400 |
Environment.Exit(1); |
401 |
} |
402 |
} |
403 |
|
404 |
static void HandleParseError(IEnumerable<Error> errs) |
405 |
{ |
406 |
errs.Output(); |
407 |
} |
408 |
|
409 |
static string GetVersionFromName(FileInfo fileInfo) |
410 |
{ |
411 |
var regexPattern = @"\d+(\.\d+)+"; |
412 |
var regex = new Regex(regexPattern); |
413 |
|
414 |
var match = regex.Match(fileInfo.FullName); |
415 |
|
416 |
return match.Captures[match.Captures.Count - 1].Value; // get the numbers at the end of the string incase the app is something like 1.0application1.0.0.dmg |
417 |
} |
418 |
|
419 |
static string GetVersionFromAssembly(FileInfo fileInfo) |
420 |
{ |
421 |
return FileVersionInfo.GetVersionInfo(fileInfo.FullName).ProductVersion; |
422 |
} |
423 |
|
424 |
|
425 |
} |
426 |
} |