Another bad commit
Lots of changes here... - Removed code using RecursivExtractor due to bad usage of /tmp Note: the code that used RecursiveExtractor may not have been in the previous commit - Created _functioning_ implementation of DebUnpacker - Restructured Project layout. TODO: - Possibly rewrite how the DebUnpacker works to have the file contents part of an entry. - Further restructuring and refactoring to make the code a little neater. - Add code for the final unpack steps Un-XZ -> untar -> write to disk - Add code to install - Add code for modified post-install pre-remove etc.. This is kinda needed to ensure proper system integration of new packages.
This commit is contained in:
parent
f650e6a3c9
commit
60e06b2144
|
|
@ -1,60 +0,0 @@
|
|||
// Experimental code to open and dissect a `.deb` archive
|
||||
|
||||
static class DebArchive
|
||||
{
|
||||
const string DPKG_AR_MAGIC = "!<arch>\n";
|
||||
const string DPKG_AR_FMAG = "`\n";
|
||||
|
||||
|
||||
private static dpkg_ar dpkg_ar_fdopen(string filename, FileInfo fi)
|
||||
{
|
||||
return new dpkg_ar(
|
||||
filename,
|
||||
fi.UnixFileMode,
|
||||
fi.Length,
|
||||
fi.LastWriteTime,
|
||||
fi);
|
||||
}
|
||||
|
||||
private static dpkg_ar dpkg_ar_open(string filename)
|
||||
{
|
||||
FileInfo fi = new FileInfo(filename);
|
||||
|
||||
return dpkg_ar_fdopen(filename, fi);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class dpkg_ar
|
||||
{
|
||||
string name;
|
||||
UnixFileMode mode;
|
||||
long size;
|
||||
DateTime time;
|
||||
FileInfo fi;
|
||||
|
||||
public dpkg_ar(string name,
|
||||
UnixFileMode mode,
|
||||
long size,
|
||||
DateTime time,
|
||||
FileInfo fi)
|
||||
{
|
||||
this.name = name;
|
||||
this.mode = mode;
|
||||
this.size = size;
|
||||
this.time = time;
|
||||
this.fi = fi;
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO:
|
||||
- Read the sourcecode for dpkg and find the c# equivalents
|
||||
to extract an archive.
|
||||
- If neccessary write a parser manually.
|
||||
*/
|
||||
/* NOTE:
|
||||
- dpkg_ar is just FileInfo
|
||||
- We might need to define dpkg_ar_hdr
|
||||
- Pretty sure we can just find line ending in "`\n"
|
||||
and parse that though. Split on (" ")
|
||||
- Should then be able to read the files by streaming `size` bytes from the position after the and marker
|
||||
*/
|
||||
145
DebUnpack.cs
Normal file
145
DebUnpack.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
using System.Text;
|
||||
using Serilog;
|
||||
namespace EdgeInstaller;
|
||||
|
||||
sealed class DebFile : IDisposable
|
||||
{
|
||||
const int HeaderBytes = 60;
|
||||
const string FileMagic = "!<arch>\n";
|
||||
|
||||
private MemoryStream _fileStream;
|
||||
|
||||
public List<DebEntry> fileEntries { get; private set; }
|
||||
|
||||
public DebFile(Stream fs)
|
||||
{
|
||||
Log.Information("Creating new debFile from stream");
|
||||
fileEntries = new();
|
||||
if (fs is null)
|
||||
{
|
||||
throw new NullReferenceException();
|
||||
}
|
||||
|
||||
fs.Position = 0;
|
||||
|
||||
// read the first 8 bytes and check for the signature
|
||||
byte[] magicBuffer = new byte[8];
|
||||
|
||||
fs.Read(magicBuffer);
|
||||
Log.Debug("Magic = {magicBuffer}", Encoding.ASCII.GetString(magicBuffer));
|
||||
if (Encoding.ASCII.GetString(magicBuffer) != FileMagic)
|
||||
{
|
||||
throw new ArgumentException(message: "Magic Fail");
|
||||
}
|
||||
|
||||
fs.Position = 0;
|
||||
_fileStream = new();
|
||||
fs.CopyTo(_fileStream);
|
||||
|
||||
fs.Dispose();
|
||||
// if we got here then we must have a proper archive.
|
||||
// We shall now get the files that are inside.
|
||||
|
||||
// from OUR copy of the stream we seek to after the magic byte
|
||||
_fileStream.Position = 8;
|
||||
while (true) // beware making an infinite loop
|
||||
{
|
||||
// Read a header
|
||||
byte[] header = new byte[60];
|
||||
var read = _fileStream.Read(header);
|
||||
// If nothing was read we have hit the end
|
||||
if (read < 60) // also handle broken headers
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Otherwise process as a header
|
||||
var entry = new DebEntry(header, _fileStream.Position);
|
||||
fileEntries.Add(entry);
|
||||
|
||||
// Using the obtained size seek forwards to the next potential header
|
||||
_fileStream.Seek(entry.sizeBytes, SeekOrigin.Current);
|
||||
}
|
||||
}
|
||||
|
||||
public bool getFile(DebEntry file, out Stream output)
|
||||
{
|
||||
// If we are given a file that isn't in this archive we exit nicely
|
||||
if (!fileEntries.Contains(file))
|
||||
{
|
||||
Log.Error("The given file entry is not valid for this DebFile");
|
||||
output = Stream.Null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we have a file that is in the archive
|
||||
// grab the metadata we need
|
||||
long offset = file.offset;
|
||||
long size = file.sizeBytes;
|
||||
// stream the file to the output stream
|
||||
_fileStream.Seek(offset, SeekOrigin.Begin);
|
||||
byte[] fileBuffer = new byte[size];
|
||||
|
||||
var count = _fileStream.Read(fileBuffer);
|
||||
if (count != size)
|
||||
{
|
||||
Log.Error("Didn't read the correct amount of data from stream (expected:{size} got:{count})", size, count);
|
||||
output = Stream.Null;
|
||||
return false;
|
||||
}
|
||||
|
||||
output = new MemoryStream(fileBuffer); // convert the read buffer to a stream
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{ // make sure we can clean up our own mess
|
||||
_fileStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DebEntry
|
||||
{
|
||||
// Escape sequnces are used in this string read as "`\n"
|
||||
const string HeaderMagic = "\x60\n";
|
||||
|
||||
public string name { get; private set; }
|
||||
public long mtime { get; private set; }
|
||||
public int mode { get; private set; }
|
||||
public int gid { get; private set; }
|
||||
public int uid { get; private set; }
|
||||
public long sizeBytes { get; private set; }
|
||||
public long offset { get; private set; } // where does the file start
|
||||
|
||||
public DebEntry(byte[] buffer, long Position)
|
||||
{
|
||||
Log.Debug("Reading header at position {p}", Position - 60); // since we are being passed a header we take 60 to get the header position
|
||||
if (buffer is null)
|
||||
{
|
||||
throw new NullReferenceException();
|
||||
}
|
||||
|
||||
if (buffer.Length < 60)
|
||||
{
|
||||
throw new ArgumentException("buffer too short");
|
||||
}
|
||||
|
||||
name = Encoding.ASCII.GetString(buffer[0..16]);
|
||||
mtime = long.Parse(Encoding.ASCII.GetString(buffer[16..28]));
|
||||
mode = int.Parse(Encoding.ASCII.GetString(buffer[28..34]));
|
||||
gid = int.Parse(Encoding.ASCII.GetString(buffer[34..40]));
|
||||
uid = int.Parse(Encoding.ASCII.GetString(buffer[40..48]));
|
||||
sizeBytes = long.Parse(Encoding.ASCII.GetString(buffer[48..58]));
|
||||
string magic = Encoding.ASCII.GetString(buffer[58..^0]);
|
||||
|
||||
offset = Position;
|
||||
|
||||
Log.Debug("Header Magic = {magic}", magic);
|
||||
Log.Debug("{@this}", this);
|
||||
if (magic != HeaderMagic)
|
||||
{
|
||||
throw new ArgumentException("magic failed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
20
Installer/Installer.cs
Normal file
20
Installer/Installer.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace EdgeInstaller;
|
||||
|
||||
// This class is what will do the heavy lifting.
|
||||
// the UI will all be handled seperately
|
||||
public class Installer
|
||||
{
|
||||
|
||||
private static readonly string DataDir =
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ClosedLess",
|
||||
"EdgeInstall-Jankmode");
|
||||
|
||||
|
||||
public Installer(HttpClient client)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
16
Package/Package.JsonConstructor.cs
Normal file
16
Package/Package.JsonConstructor.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public sealed partial class Package
|
||||
{
|
||||
[JsonConstructor] // NOTE: without this it would be impossible to deserialise a Package
|
||||
private Package(string PackageName, Version Version, int Size, string SHA256, string Filename)
|
||||
{
|
||||
this.PackageName = PackageName;
|
||||
this.Version = Version;
|
||||
this.Size = Size;
|
||||
this.SHA256 = SHA256;
|
||||
this.Filename = Filename;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using Serilog;
|
||||
using System.Text.Json.Serialization;
|
||||
// Defines a Package
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
sealed class Package
|
||||
sealed partial class Package
|
||||
{
|
||||
|
||||
public string PackageName { get; private set; }
|
||||
|
|
@ -12,16 +12,6 @@ sealed class Package
|
|||
public string SHA256 { get; private set; }
|
||||
public string Filename { get; private set; }
|
||||
|
||||
[JsonConstructor] // NOTE: without this it would be impossible to deserialise a Package
|
||||
private Package(string PackageName, Version Version, int Size, string SHA256, string Filename)
|
||||
{
|
||||
this.PackageName = PackageName;
|
||||
this.Version = Version;
|
||||
this.Size = Size;
|
||||
this.SHA256 = SHA256;
|
||||
this.Filename = Filename;
|
||||
}
|
||||
|
||||
public Package(string packageString)
|
||||
{
|
||||
Log.Information("Creating package from string");
|
||||
|
|
@ -61,6 +51,4 @@ sealed class Package
|
|||
|
||||
/* TODO: capture dependencies and create method to find them.
|
||||
- This would probably need distro specific lookup tables.
|
||||
- Also would ideally need code to shell out (at root level)
|
||||
to perform installs...
|
||||
*/
|
||||
126
Program.Downloaders.cs
Normal file
126
Program.Downloaders.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using Spectre.Console;
|
||||
using Serilog;
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public static partial class EdgeInstall
|
||||
{
|
||||
|
||||
|
||||
private static async Task<string> GetRelease()
|
||||
{
|
||||
string releaseURL = "dists/stable/Release";
|
||||
using HttpResponseMessage response = await _client.GetAsync(releaseURL);
|
||||
await Task.Delay(5000);
|
||||
Log.Information("Getting release from {url}", response.Headers.Location);
|
||||
response.EnsureSuccessStatusCode(); //WARN: this can throw an exception, Please wrap in try/catch or rewrite function
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Log.Debug("{body}", body);
|
||||
return body;
|
||||
}
|
||||
|
||||
static async Task<string> downloadFile(string URL, string DestinationDirectory)
|
||||
{
|
||||
|
||||
Uri file = new Uri(URL);
|
||||
|
||||
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
|
||||
|
||||
using HttpResponseMessage response = await _client.GetAsync(URL);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
||||
var fileData = response.Content.ReadAsStreamAsync();
|
||||
|
||||
using var fileStream = new FileStream(fileLocation, FileMode.OpenOrCreate);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
|
||||
// This overload of downloadFile allows you to get the progess
|
||||
static async Task<string> downloadFile(string URL, string DestinationDirectory, ProgressTask task)
|
||||
{
|
||||
Uri file = new Uri(URL, UriKind.RelativeOrAbsolute); // allow this to work regardless of relativity
|
||||
|
||||
if (!file.IsAbsoluteUri && _client.BaseAddress is not null)
|
||||
{ // Ego te absolvo
|
||||
file = new Uri(_client.BaseAddress, URL);
|
||||
}
|
||||
|
||||
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
|
||||
|
||||
using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
task.MaxValue = response.Content.Headers.ContentLength ?? 0;
|
||||
task.StartTask();
|
||||
await Task.Delay(2000);
|
||||
|
||||
AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] ({task.MaxValue} Bytes)");
|
||||
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync())
|
||||
using (var fileStream = File.Open(fileLocation, FileMode.OpenOrCreate))
|
||||
{
|
||||
var buffer = new byte[downloadBufferBYTES];
|
||||
while (true)
|
||||
{
|
||||
var read = await contentStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
await Task.Delay(downloadDelayMS);
|
||||
if (read == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]");
|
||||
break;
|
||||
}
|
||||
task.Increment(read);
|
||||
|
||||
await fileStream.WriteAsync(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
// This overload of downloadFile allows you to get the progess
|
||||
static async Task<MemoryStream> downloadStream(string URL, ProgressTask task)
|
||||
{
|
||||
MemoryStream outputStream = new();
|
||||
using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
task.MaxValue = response.Content.Headers.ContentLength ?? 0;
|
||||
task.StartTask();
|
||||
await Task.Delay(2000);
|
||||
|
||||
AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] to memory ({task.MaxValue} Bytes)");
|
||||
|
||||
int bufferSize = (int)Math.Round(task.MaxValue / 100);
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync())
|
||||
{
|
||||
var buffer = new byte[bufferSize];
|
||||
while (true)
|
||||
{
|
||||
var read = await contentStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
await Task.Delay(downloadDelayMS);
|
||||
if (read == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]");
|
||||
break;
|
||||
}
|
||||
task.Increment(read);
|
||||
|
||||
await outputStream.WriteAsync(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputStream;
|
||||
}
|
||||
}
|
||||
67
Program.HelperFunctions.cs
Normal file
67
Program.HelperFunctions.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Serilog;
|
||||
using Spectre.Console;
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public static partial class EdgeInstall
|
||||
{
|
||||
private static string GetPackagesString(string filepath)
|
||||
{
|
||||
using (var CompressedStream = File.Open(filepath, FileMode.Open))
|
||||
using (var decompressor = new GZipStream(CompressedStream, CompressionMode.Decompress))
|
||||
using (var streamReader = new StreamReader(decompressor))
|
||||
{
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* HACK: Spectre.Console uses fixed regex to detect terminal capabilites
|
||||
* This function tests for known working terminals that are currently not
|
||||
* detected by AnsiDetector
|
||||
*/
|
||||
private static void BUGFIX_ansiDetection()
|
||||
{
|
||||
Regex[] regexes = {
|
||||
new("foot")
|
||||
};
|
||||
AnsiConsole.Profile.Capabilities.Ansi = true;
|
||||
AnsiConsole.Profile.Capabilities.Legacy = false;
|
||||
}
|
||||
|
||||
static bool ValidateChecksum(FileInfo file, string checksum)
|
||||
{
|
||||
using (FileStream fs = file.Open(FileMode.Open))
|
||||
{
|
||||
return ValidateChecksum(fs, checksum);
|
||||
}
|
||||
}
|
||||
|
||||
static bool ValidateChecksum(Stream stream, string checksum)
|
||||
{
|
||||
stream.Position = 0;
|
||||
using (SHA256 mySHA256 = SHA256.Create())
|
||||
{
|
||||
byte[] hashValue = mySHA256.ComputeHash(stream);
|
||||
if (hashValue is null)
|
||||
{
|
||||
throw new NullReferenceException("Hash be null??");
|
||||
}
|
||||
Log.Debug("Computed hash = {hash}", hashValue);
|
||||
byte[] storedHash = Convert.FromHexString(checksum);
|
||||
Log.Debug("Stored Hash = {hash}", storedHash);
|
||||
|
||||
Log.Debug("Compare result = {result}", hashValue.SequenceEqual(storedHash));
|
||||
return (storedHash.SequenceEqual(hashValue));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static Version GetCurrentVersion()
|
||||
{
|
||||
|
||||
return new("0.0.0.0-0");
|
||||
}
|
||||
}
|
||||
150
Program.Renderers.cs
Normal file
150
Program.Renderers.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
using Spectre.Console;
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public static partial class EdgeInstall
|
||||
{
|
||||
|
||||
static void RenderPackageAsTable(Package package)
|
||||
{
|
||||
Table Package = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Rounded)
|
||||
.HideHeaders()
|
||||
.Title("Package To Install");
|
||||
|
||||
AnsiConsole.Live(Package)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
Package.AddColumns("", "");
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
|
||||
Package.AddRow("[bold yellow]Package Name : [/]", $"{package.PackageName}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Version : [/]", $"{package.Version.ToString()}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Size : [/]", $"{package.Size.ToString()}B");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]SHA256 : [/]", $"{package.SHA256}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Filename : [/]", $"{package.Filename}");
|
||||
context.Refresh();
|
||||
});
|
||||
}
|
||||
|
||||
static void RenderPackagesTable(List<Package> Packages)
|
||||
{
|
||||
Table PackageTable = new Table()
|
||||
.Centered()
|
||||
.Expand()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Title("Packages");
|
||||
|
||||
AnsiConsole.Live(PackageTable)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
PackageTable
|
||||
.AddColumn("[bold]PackageName[/]")
|
||||
.AddColumn("[bold]Version[/]")
|
||||
.AddColumn("[bold]Size (bytes)[/]")
|
||||
.AddColumn("[bold]SHA256[/]")
|
||||
.AddColumn("[bold]Filename[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
foreach (var pack in Packages)
|
||||
{
|
||||
PackageTable.AddRow(
|
||||
FormatPackageName(pack.PackageName),
|
||||
$"[i blue]{pack.Version.ToString()}[/]",
|
||||
$"[grey]{pack.Size.ToString()}[/][b]B[/]",
|
||||
pack.SHA256,
|
||||
pack.Filename);
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static string FormatPackageName(string name)
|
||||
{
|
||||
if (name.Contains("dev"))
|
||||
{
|
||||
return $"[red]{name}[/]";
|
||||
}
|
||||
if (name.Contains("beta"))
|
||||
{
|
||||
return $"[yellow]{name}[/]";
|
||||
}
|
||||
if (name.Contains("stable"))
|
||||
{
|
||||
return $"[green]{name}[/]";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
|
||||
static void RenderReleaseTable(ReleaseData data)
|
||||
{
|
||||
// Lets output this ReleaseData now
|
||||
Table ReleaseTable = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Expand()
|
||||
.Title("Release Data");
|
||||
|
||||
AnsiConsole.Live(ReleaseTable)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
ReleaseTable.AddColumn("[bold]Field[/]");
|
||||
ReleaseTable.AddColumn("[bold]Value[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
ReleaseTable.AddRow("[bold yellow]Date:[/]", $"[blue]{data.Date:R}[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
Table archs = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.None)
|
||||
.HideFooters()
|
||||
.HideHeaders()
|
||||
.AddColumn(new TableColumn(Text.Empty));
|
||||
ReleaseTable.AddRow(new Markup("[bold yellow]Architectures:[/]"), archs);
|
||||
foreach (var arch in data.Architectures)
|
||||
{
|
||||
archs.AddRow(arch);
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
|
||||
Table PackageFilesTable = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Square)
|
||||
.BorderColor(Color.Red)
|
||||
.AddColumn("[bold]Filename[/]")
|
||||
.AddColumn("[bold]Checksum[/]")
|
||||
.AddColumn("[bold]Size[/]");
|
||||
ReleaseTable.AddRow(
|
||||
new Markup("[bold yellow]Package Files:[/]"),
|
||||
PackageFilesTable);
|
||||
context.Refresh();
|
||||
foreach (var PackageFile in data.PackageFiles)
|
||||
{
|
||||
PackageFilesTable.AddRow(PackageFile.filename, PackageFile.Checksum, PackageFile.size.ToString());
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
405
Program.cs
405
Program.cs
|
|
@ -1,15 +1,22 @@
|
|||
//using System.CommandLine;
|
||||
using Serilog;
|
||||
using Spectre.Console;
|
||||
using System.IO.Compression;
|
||||
using System.Text.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public static class EdgeInstall
|
||||
public static partial class EdgeInstall
|
||||
{
|
||||
// NOTE: tweaking these values can be used to make downloads look more "fancy"
|
||||
// A.K.A make downloads take longer so the progress bar appears for a little longer
|
||||
private static readonly int downloadDelayMS = 0;
|
||||
private static readonly int downloadBufferBYTES = 10240;
|
||||
|
||||
// The default architecture to get
|
||||
// likely will go unused until proper cli interface is added
|
||||
private static readonly string _DefaultArch = "amd64";
|
||||
|
||||
private static HttpClient _client = new HttpClient();
|
||||
|
||||
private static readonly string DataDir =
|
||||
|
|
@ -18,23 +25,19 @@ public static class EdgeInstall
|
|||
"ClosedLess",
|
||||
"EdgeInstall-Jankmode");
|
||||
|
||||
|
||||
public static async Task<int> Main()
|
||||
{
|
||||
// WARN: Hardcoded variables are bad m'kay...
|
||||
string Arch = "amd64";
|
||||
|
||||
_client.BaseAddress = new Uri("https://packages.microsoft.com/repos/edge/");
|
||||
// Ensure DataDir exists.
|
||||
// NOTE: Directory.CreateDirectory will ALWAYS create the entire path
|
||||
Directory.CreateDirectory(DataDir);
|
||||
|
||||
// TODO: wrap this code in foot detection since foot isn't detected properly
|
||||
// BUG: Report detection error to Spectre.Console (foot == xterm)
|
||||
// HACK: We have to force this right now for foot to work properly...
|
||||
AnsiConsole.Console.Profile.Capabilities.Ansi = true;
|
||||
BUGFIX_ansiDetection();
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(".EdgeInstaller.Log") // Only ever log to the logfile
|
||||
.WriteTo.File(Path.Combine(DataDir, ".EdgeInstaller.Log")) // Only ever log to the logfile
|
||||
.CreateLogger();
|
||||
|
||||
var Release = AnsiConsole.Status()
|
||||
|
|
@ -49,26 +52,26 @@ public static class EdgeInstall
|
|||
Log.Debug("{rs}", ReleaseString);
|
||||
|
||||
AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]");
|
||||
ReleaseData data = getReleaseData(ReleaseString);
|
||||
ReleaseData data = new(ReleaseString);
|
||||
|
||||
RenderReleaseTable(data);
|
||||
|
||||
Arch = AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose Architecture")
|
||||
.PageSize(10)
|
||||
.MoreChoicesText("[grey]Scroll for more...[/]")
|
||||
.AddChoices(data.Architectures)
|
||||
);
|
||||
// only select arch if needed
|
||||
string TargetArch = data.Architectures.Count > 1 ? AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose Architecture")
|
||||
.PageSize(5)
|
||||
.MoreChoicesText("[grey]Scroll for more...[/]")
|
||||
.AddChoices(data.Architectures)
|
||||
) : data.Architectures.First();
|
||||
|
||||
// create a directory for the chosen architecture
|
||||
// NOTE: we also grab the path here so we can use it later.
|
||||
var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, Arch)).FullName;
|
||||
var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, TargetArch)).FullName;
|
||||
|
||||
// Check if the Release data we just grabbed is newer than the one that exists.
|
||||
// NOTE: we only do this if our chosen architecture hasn't been downloaded
|
||||
|
||||
// OOF semaphores..
|
||||
bool NeedDownload = true;
|
||||
|
||||
Log.Information("Reached Package download Stage");
|
||||
|
|
@ -98,34 +101,34 @@ public static class EdgeInstall
|
|||
.Where(
|
||||
file =>
|
||||
file.filename.Contains(".gz") &&
|
||||
file.filename.Contains(Arch)) // ensure we only fetch for the chosen architecture
|
||||
file.filename.Contains(TargetArch)) // ensure we only fetch for the chosen architecture
|
||||
.First();
|
||||
if (gzFile is null)
|
||||
{
|
||||
throw new NullReferenceException("OOPS gzfile null");
|
||||
}
|
||||
string URL = "http://packages.microsoft.com/repos/edge/dists/stable/" + gzFile.filename;
|
||||
string URL = "dists/stable/" + gzFile.filename;
|
||||
|
||||
string filename = await AnsiConsole.Progress()
|
||||
.Columns(new ProgressColumn[]
|
||||
{
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn(),
|
||||
new PercentageColumn(),
|
||||
new RemainingTimeColumn(),
|
||||
new SpinnerColumn(),
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn(),
|
||||
new PercentageColumn(),
|
||||
new RemainingTimeColumn(),
|
||||
new SpinnerColumn(),
|
||||
})
|
||||
.StartAsync<string>(async ctx =>
|
||||
{
|
||||
var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false });
|
||||
.StartAsync<string>(async ctx =>
|
||||
{
|
||||
var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false });
|
||||
|
||||
return await downloadFile(URL, Archdir, task);
|
||||
return await downloadFile(URL, Archdir, task);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
string JSONdata = JsonSerializer.Serialize<ReleaseData>(data);
|
||||
File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata);
|
||||
if (!ValidateChecksum(filename, gzFile.Checksum))
|
||||
if (!ValidateChecksum(new FileInfo(filename), gzFile.Checksum))
|
||||
{
|
||||
File.Delete(filename);
|
||||
Environment.Exit(1);
|
||||
|
|
@ -145,6 +148,10 @@ public static class EdgeInstall
|
|||
if (line == "")
|
||||
{
|
||||
string PackageString = sb.ToString();
|
||||
if (PackageString == "")
|
||||
{
|
||||
continue; // The last "package" will be empty
|
||||
}
|
||||
sb.Clear(); // ensure we clear the StringBuilder when we are done with it.
|
||||
|
||||
try
|
||||
|
|
@ -171,15 +178,15 @@ public static class EdgeInstall
|
|||
.ThenByDescending(package => package.Version).ToList();
|
||||
// Print any packages we have
|
||||
var Variant = AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose variant")
|
||||
.PageSize(4)
|
||||
.AddChoices(new[] {
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose variant")
|
||||
.PageSize(4)
|
||||
.AddChoices(new[] {
|
||||
"stable",
|
||||
"beta",
|
||||
"dev",
|
||||
"all"
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
|
@ -191,19 +198,19 @@ public static class EdgeInstall
|
|||
else
|
||||
{
|
||||
VariantPackages = packages
|
||||
.Where(package =>
|
||||
.Where(package =>
|
||||
package.PackageName.Contains(Variant))
|
||||
.ToList();
|
||||
.ToList();
|
||||
}
|
||||
|
||||
RenderPackagesTable(VariantPackages);
|
||||
|
||||
|
||||
var Package = AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose Package to Install")
|
||||
.PageSize(5)
|
||||
.AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct())
|
||||
new SelectionPrompt<string>()
|
||||
.Title("Choose Package to Install")
|
||||
.PageSize(5)
|
||||
.AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct())
|
||||
);
|
||||
|
||||
var ChosenPackages = packages
|
||||
|
|
@ -215,261 +222,77 @@ public static class EdgeInstall
|
|||
|
||||
RenderPackageAsTable(LatestVersion);
|
||||
|
||||
return 0;
|
||||
}
|
||||
var versionPath = LatestVersion.Filename
|
||||
.Replace("pool/main/m/", "")
|
||||
.Replace(".deb", "");
|
||||
|
||||
static void RenderPackageAsTable(Package package)
|
||||
{
|
||||
Table Package = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Rounded)
|
||||
.HideHeaders()
|
||||
.Title("Package To Install");
|
||||
var versionsDir = Path.Join(Archdir, "Versions");
|
||||
// Let's get and unpack the latest version
|
||||
|
||||
AnsiConsole.Live(Package)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
Package.AddColumns("", "");
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
|
||||
Package.AddRow("[bold yellow]Package Name : [/]", $"{package.PackageName}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Version : [/]", $"{package.Version.ToString()}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Size : [/]", $"{package.Size.ToString()}B");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]SHA256 : [/]", $"{package.SHA256}");
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
Package.AddRow("[bold yellow]Filename : [/]", $"{package.Filename}");
|
||||
context.Refresh();
|
||||
});
|
||||
}
|
||||
|
||||
static void RenderPackagesTable(List<Package> Packages)
|
||||
{
|
||||
Table PackageTable = new Table()
|
||||
.Centered()
|
||||
.Expand()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Title("Packages");
|
||||
|
||||
AnsiConsole.Live(PackageTable)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
PackageTable
|
||||
.AddColumn("[bold]PackageName[/]")
|
||||
.AddColumn("[bold]Version[/]")
|
||||
.AddColumn("[bold]Size (bytes)[/]")
|
||||
.AddColumn("[bold]SHA256[/]")
|
||||
.AddColumn("[bold]Filename[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
foreach (var pack in Packages)
|
||||
{
|
||||
PackageTable.AddRow(FormatPackageName(pack.PackageName), $"[i blue]{pack.Version.ToString()}[/]", $"[grey]{pack.Size.ToString()}[/][b]B[/]", pack.SHA256, pack.Filename);
|
||||
context.Refresh();
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
static string FormatPackageName(string name)
|
||||
{
|
||||
if (name.Contains("dev"))
|
||||
if (true)//!Directory.Exists(Path.Join(versionsDir, versionPath)))
|
||||
{
|
||||
return $"[red]{name}[/]";
|
||||
}
|
||||
if (name.Contains("beta"))
|
||||
{
|
||||
return $"[yellow]{name}[/]";
|
||||
}
|
||||
if (name.Contains("stable"))
|
||||
{
|
||||
return $"[green]{name}[/]";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
static async Task<string> GetRelease()
|
||||
{
|
||||
string releaseURL = "http://packages.microsoft.com/repos/edge/dists/stable/Release";
|
||||
using HttpResponseMessage response = await _client.GetAsync(releaseURL);
|
||||
await Task.Delay(5000);
|
||||
response.EnsureSuccessStatusCode(); // WARNING: this can throw an exception, Please wrap in try/catch or rewrite function
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Log.Debug("{body}", body);
|
||||
return body;
|
||||
}
|
||||
|
||||
static ReleaseData getReleaseData(string release)
|
||||
{
|
||||
return new ReleaseData(release);
|
||||
}
|
||||
|
||||
static async Task<string> downloadFile(string URL, string DestinationDirectory)
|
||||
{
|
||||
|
||||
Uri file = new Uri(URL);
|
||||
|
||||
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
|
||||
|
||||
using HttpResponseMessage response = await _client.GetAsync(URL);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await Task.Delay(2000);
|
||||
|
||||
var fileData = response.Content.ReadAsStreamAsync();
|
||||
|
||||
using var fileStream = new FileStream(fileLocation, FileMode.OpenOrCreate);
|
||||
await response.Content.CopyToAsync(fileStream);
|
||||
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
static async Task<string> downloadFile(string URL, string DestinationDirectory, ProgressTask task)
|
||||
{
|
||||
Uri file = new Uri(URL);
|
||||
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
|
||||
|
||||
using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
task.MaxValue = response.Content.Headers.ContentLength ?? 0;
|
||||
task.StartTask();
|
||||
await Task.Delay(2000);
|
||||
|
||||
AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] ({task.MaxValue} Bytes)");
|
||||
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync())
|
||||
using (var fileStream = File.Open(fileLocation, FileMode.OpenOrCreate))
|
||||
{
|
||||
var buffer = new byte[512];
|
||||
while (true)
|
||||
{
|
||||
var read = await contentStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
await Task.Delay(500);
|
||||
if (read == 0)
|
||||
using (MemoryStream debStream =
|
||||
await AnsiConsole.Progress()
|
||||
.Columns(new ProgressColumn[]
|
||||
{
|
||||
new TaskDescriptionColumn(),
|
||||
new ProgressBarColumn(),
|
||||
new PercentageColumn(),
|
||||
new RemainingTimeColumn(),
|
||||
new SpinnerColumn(),
|
||||
})
|
||||
.StartAsync<MemoryStream>(async ctx =>
|
||||
{
|
||||
AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]");
|
||||
break;
|
||||
var task = ctx.AddTask(LatestVersion.Filename, new ProgressTaskSettings { AutoStart = false });
|
||||
|
||||
return await downloadStream(LatestVersion.Filename, task);
|
||||
}))
|
||||
{
|
||||
|
||||
if (!ValidateChecksum(debStream, LatestVersion.SHA256))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red b]ERROR:[/] checksum did not validate");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
using (DebFile debfile = new DebFile(debStream))
|
||||
{
|
||||
foreach (var fileEntry in debfile.fileEntries)
|
||||
{
|
||||
AnsiConsole.MarkupLineInterpolated($"FileName: {fileEntry.name}");
|
||||
AnsiConsole.MarkupLineInterpolated($"FilePos: {fileEntry.offset}");
|
||||
AnsiConsole.MarkupLineInterpolated($"FileSize: {fileEntry.sizeBytes}");
|
||||
|
||||
// NOTE: This is dumb test code
|
||||
// to verify the files are actually being "extracted" properly
|
||||
|
||||
if (!debfile.getFile(fileEntry, out Stream contents))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Now we write this to tmp.
|
||||
// WARN: Without deleting these you will fill up your ram
|
||||
|
||||
var temp = Path.GetTempFileName();
|
||||
|
||||
using (var tmpfile = File.Open(temp, FileMode.Open))
|
||||
{
|
||||
contents.CopyTo(tmpfile);
|
||||
contents.Dispose();
|
||||
}
|
||||
}
|
||||
task.Increment(read);
|
||||
|
||||
await fileStream.WriteAsync(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileLocation;
|
||||
}
|
||||
|
||||
static bool ValidateChecksum(string path, string checksum)
|
||||
{
|
||||
using (SHA256 mySHA256 = SHA256.Create())
|
||||
{
|
||||
using (FileStream fs = File.Open(path, FileMode.Open))
|
||||
{
|
||||
fs.Position = 0;
|
||||
|
||||
byte[] hashValue = mySHA256.ComputeHash(fs);
|
||||
if (hashValue is null)
|
||||
{
|
||||
throw new NullReferenceException("Hash be null??");
|
||||
}
|
||||
Log.Debug("Computed hash = {hash}", hashValue);
|
||||
byte[] storedHash = Convert.FromHexString(checksum);
|
||||
Log.Debug("Stored Hash = {hash}", storedHash);
|
||||
|
||||
Log.Debug("Compare result = {result}", hashValue.SequenceEqual(storedHash));
|
||||
return (storedHash.SequenceEqual(hashValue));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void RenderReleaseTable(ReleaseData data)
|
||||
{
|
||||
// Lets output this ReleaseData now
|
||||
Table ReleaseTable = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Rounded)
|
||||
.Expand()
|
||||
.Title("Release Data");
|
||||
|
||||
AnsiConsole.Live(ReleaseTable)
|
||||
.AutoClear(false)
|
||||
.Start(context =>
|
||||
{
|
||||
ReleaseTable.AddColumn("[bold]Field[/]");
|
||||
ReleaseTable.AddColumn("[bold]Value[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
ReleaseTable.AddRow("[bold yellow]Date:[/]", $"[blue]{data.Date:R}[/]");
|
||||
context.Refresh();
|
||||
Thread.Sleep(500);
|
||||
|
||||
Table archs = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.None)
|
||||
.HideFooters()
|
||||
.HideHeaders()
|
||||
.AddColumn(new TableColumn(Text.Empty));
|
||||
ReleaseTable.AddRow(new Markup("[bold yellow]Architectures:[/]"), archs);
|
||||
foreach (var arch in data.Architectures)
|
||||
{
|
||||
archs.AddRow(arch);
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
|
||||
Table PackageFilesTable = new Table()
|
||||
.Centered()
|
||||
.Border(TableBorder.Square)
|
||||
.BorderColor(Color.Red)
|
||||
.AddColumn("[bold]Filename[/]")
|
||||
.AddColumn("[bold]Checksum[/]")
|
||||
.AddColumn("[bold]Size[/]");
|
||||
ReleaseTable.AddRow(
|
||||
new Markup("[bold yellow]Package Files:[/]"),
|
||||
PackageFilesTable);
|
||||
context.Refresh();
|
||||
foreach (var PackageFile in data.PackageFiles)
|
||||
{
|
||||
PackageFilesTable.AddRow(PackageFile.filename, PackageFile.Checksum, PackageFile.size.ToString());
|
||||
context.Refresh();
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetPackagesString(string filepath)
|
||||
{
|
||||
using (var CompressedStream = File.Open(filepath, FileMode.Open))
|
||||
using (var decompressor = new GZipStream(CompressedStream, CompressionMode.Decompress))
|
||||
using (var streamReader = new StreamReader(decompressor))
|
||||
{
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/*TODO:
|
||||
- Parse package to find latest version
|
||||
- Check version
|
||||
- Offer update
|
||||
- Make SENSIBLE backup
|
||||
- Do install
|
||||
= if install fails revert backup
|
||||
*/
|
||||
- Check version
|
||||
- Offer update
|
||||
- Make SENSIBLE backup
|
||||
- Do install
|
||||
= if install fails revert backup
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
13
ReleaseData/ReleaseData.JsonConstructor.cs
Normal file
13
ReleaseData/ReleaseData.JsonConstructor.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
namespace EdgeInstaller;
|
||||
|
||||
public sealed partial class ReleaseData
|
||||
{
|
||||
[JsonConstructor]
|
||||
public ReleaseData(DateTime date, List<string> architectures, List<PackageFile> packageFiles)
|
||||
{
|
||||
this.Date = date;
|
||||
this.Architectures = architectures;
|
||||
this.PackageFiles = packageFiles;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,22 +2,15 @@
|
|||
// Substring should be replaced with a .split(": ")
|
||||
// Also Directly assign to the values instead of using temporary variables.
|
||||
using Serilog;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
sealed class ReleaseData
|
||||
public sealed partial class ReleaseData
|
||||
{
|
||||
public DateTime Date { get; private set; }
|
||||
public List<string> Architectures { get; private set; }
|
||||
public List<PackageFile> PackageFiles { get; private set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public ReleaseData(DateTime date, List<string> architectures, List<PackageFile> packageFiles)
|
||||
{
|
||||
this.Date = date;
|
||||
this.Architectures = architectures;
|
||||
this.PackageFiles = packageFiles;
|
||||
}
|
||||
|
||||
public ReleaseData(string Release)
|
||||
{
|
||||
|
|
@ -30,31 +23,19 @@ sealed class ReleaseData
|
|||
paramName: "Release");
|
||||
}
|
||||
|
||||
Architectures = new();
|
||||
PackageFiles = new();
|
||||
|
||||
// Pull release apart
|
||||
List<string> ReleaseLines = Release.Split("\n", StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
Log.Debug("ReleaseLines = {@releaselines}", ReleaseLines);
|
||||
|
||||
// Safely find and parse the Date field
|
||||
string DateLine =
|
||||
ReleaseLines.Where(
|
||||
Date = DateTime.Parse(
|
||||
ReleaseLines.Where(
|
||||
Line => Line.Contains("Date:"))
|
||||
.FirstOrDefault("").Substring(5);
|
||||
Log.Debug("DateLine = {DateLine}", DateLine);
|
||||
|
||||
|
||||
if (DateTime.TryParse(DateLine, out DateTime result))
|
||||
{
|
||||
Log.Debug("Extracted date {@date}", result);
|
||||
Date = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw _lineFormatException("date");
|
||||
}
|
||||
.First()
|
||||
.Split(": ")
|
||||
.Last()
|
||||
);
|
||||
|
||||
// Safely find and add the possible architectures
|
||||
string ArchitecturesLine =
|
||||
|
|
@ -67,19 +48,25 @@ sealed class ReleaseData
|
|||
throw _lineFormatException("architectures");
|
||||
}
|
||||
Architectures = ArchitecturesLine
|
||||
.Substring(ArchitecturesLine.IndexOf(":") + 1) // We only want items after the label
|
||||
.Split(" ", StringSplitOptions.RemoveEmptyEntries) // Space delimited list
|
||||
.ToList(); // Convert to the superior collection type
|
||||
.Split(": ").Last() // We only want items after the label
|
||||
.Split(" ", StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
Log.Debug("Extracted architectures {@architectures}", Architectures);
|
||||
|
||||
// Time to process the package files.. This is gonna be fun..
|
||||
// WARNING: this code is a little hacky
|
||||
// scope for possible optimisation
|
||||
|
||||
/*
|
||||
* Package files are grabbed here. It is worth noting that empty files
|
||||
* are removed (along with the .gz versions) this avoids displaying impossible
|
||||
* options to the user
|
||||
*/
|
||||
|
||||
|
||||
// First split the Lines to only get the file data
|
||||
List<string> SHA256FileData = ReleaseLines.Where((value, index) =>
|
||||
index > ReleaseLines.IndexOf("SHA256:") && index < ReleaseLines.IndexOf("SHA512:")
|
||||
).ToList();
|
||||
// NOTE: This is probably the cleanest way to do this.
|
||||
List<string> SHA256FileData = ReleaseLines.Where((_, index) =>
|
||||
index > ReleaseLines.IndexOf("SHA256:") &&
|
||||
index < ReleaseLines.IndexOf("SHA512:")
|
||||
).ToList();
|
||||
Log.Debug("SHA256FileData = {@data}", SHA256FileData);
|
||||
|
||||
// Now process these lines to get the data we need
|
||||
|
|
@ -96,16 +83,19 @@ sealed class ReleaseData
|
|||
}
|
||||
|
||||
// remove any empty files (and the .gz version of them)
|
||||
// NOTE: I do this since empty gz files have a size
|
||||
// NOTE: Empty gz files still have a size due to headers
|
||||
// WARN: The `.ToList` is essential to ensure we COPY the list and not use references to the original items
|
||||
var emptyFiles = releasePackageFiles.Where(package => package.size == 0).ToList();
|
||||
var emptyFiles = releasePackageFiles
|
||||
.Where(package => package.size == 0)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in emptyFiles)
|
||||
{
|
||||
releasePackageFiles.RemoveAll(package =>
|
||||
releasePackageFiles
|
||||
.RemoveAll(package =>
|
||||
package.filename == file.filename ||
|
||||
package.filename == $"{file.filename}.gz"
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
PackageFiles = releasePackageFiles;
|
||||
|
|
@ -113,7 +103,11 @@ sealed class ReleaseData
|
|||
// After removing the empty files we need to make sure that we remove the architectures that don't have any files
|
||||
|
||||
foreach (var arch in Architectures.ToList())
|
||||
// WARN: `.ToList` is used again to copy the list so we can iterate and operate
|
||||
/*
|
||||
* WARN: `.ToList` is used again to copy the list so we can iterate and operate
|
||||
* on the list at the same time. This could be unsafe, but in this scenario
|
||||
* should never cause any issues.
|
||||
*/
|
||||
{
|
||||
if (PackageFiles.Where(file => file.filename.Contains(arch)).Count() == 0)
|
||||
{
|
||||
127
Version.cs
127
Version.cs
|
|
@ -1,127 +0,0 @@
|
|||
using Serilog;
|
||||
// Make a version since it is so much easier to compare when the class handles it.
|
||||
|
||||
sealed class Version : IComparable<Version>, IEquatable<Version>
|
||||
{
|
||||
public int Major { get; private set; }
|
||||
public int Minor { get; private set; }
|
||||
public int Build { get; private set; }
|
||||
public int Patch { get; private set; }
|
||||
public int Subpatch { get; private set; }
|
||||
|
||||
|
||||
public Version(string versionString)
|
||||
{
|
||||
Log.Information("Creating version from string {versionString}", versionString);
|
||||
if (versionString == "")
|
||||
{
|
||||
throw new ArgumentException("The passed version string was empty", "versionString");
|
||||
}
|
||||
|
||||
var splitVersion = versionString.Split('.');
|
||||
Log.Debug("SplitVersion = {@splitversion}", splitVersion);
|
||||
Major = int.Parse(splitVersion[0]);
|
||||
Minor = int.Parse(splitVersion[1]);
|
||||
Build = int.Parse(splitVersion[2]);
|
||||
|
||||
var splitPatch = splitVersion[3].Split('-');
|
||||
Log.Debug("SplitPatch = {@splitPatch}", splitPatch);
|
||||
Log.Debug("SplitPatch values : [0] = {0}, [1] = {1}", splitPatch[0], splitPatch[1]);
|
||||
Patch = int.Parse(splitPatch[0]);
|
||||
Subpatch = int.Parse(splitPatch[1]);
|
||||
Log.Debug("Version Created: {@version}", this);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Major}.{Minor}.{Build}.{Patch}-{Subpatch}";
|
||||
}
|
||||
|
||||
public int CompareTo(Version? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// full Equality is easy since everything should be the same.
|
||||
// OPTIM: Doing this here is techinally more effecient than baking it in to the later code
|
||||
// Bools and shit
|
||||
if (this.Major == other.Major &&
|
||||
this.Minor == other.Minor &&
|
||||
this.Build == other.Build &&
|
||||
this.Patch == other.Patch &&
|
||||
this.Subpatch == other.Subpatch)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Compare major version
|
||||
if (Major != other.Major)
|
||||
return Major.CompareTo(other.Major);
|
||||
|
||||
// Compare minor version
|
||||
if (Minor != other.Minor)
|
||||
return Minor.CompareTo(other.Minor);
|
||||
|
||||
// Compare build number
|
||||
if (Build != other.Build)
|
||||
return Build.CompareTo(other.Build);
|
||||
|
||||
// Compare patch number
|
||||
if (Patch != other.Patch)
|
||||
return Patch.CompareTo(other.Patch);
|
||||
|
||||
// Compare subpatch number
|
||||
return Subpatch.CompareTo(other.Subpatch);
|
||||
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is null || !this.GetType().Equals(obj.GetType()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.Equals(obj);
|
||||
// NOTE: For the purposes of a version number same values == same version
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Major + Minor + Build + Patch + Subpatch;
|
||||
// NOTE: For the purposes of a version number same values == same version
|
||||
// That is why this "hash" code will collide given two "different" versions with the same values
|
||||
}
|
||||
|
||||
public bool Equals(Version? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.CompareTo(other) == 0;
|
||||
}
|
||||
|
||||
public static bool operator >(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) > 0;
|
||||
}
|
||||
|
||||
public static bool operator <(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) < 0;
|
||||
}
|
||||
|
||||
|
||||
public static bool operator >=(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) >= 0;
|
||||
}
|
||||
|
||||
public static bool operator <=(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) <= 0;
|
||||
}
|
||||
}
|
||||
75
Version/Version.IComparable.cs
Normal file
75
Version/Version.IComparable.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
namespace EdgeInstaller;
|
||||
|
||||
public sealed partial class Version : IComparable<Version>
|
||||
{
|
||||
public int CompareTo(Version? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// full Equality is easy since everything should be the same.
|
||||
// OPTIM: Doing this here is techinally more effecient than baking it in to the later code
|
||||
// Bools and shit
|
||||
if (this.Major == other.Major &&
|
||||
this.Minor == other.Minor &&
|
||||
this.Build == other.Build &&
|
||||
this.Patch == other.Patch &&
|
||||
this.Subpatch == other.Subpatch)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Compare major version
|
||||
if (Major != other.Major)
|
||||
return Major.CompareTo(other.Major);
|
||||
|
||||
// Compare minor version
|
||||
if (Minor != other.Minor)
|
||||
return Minor.CompareTo(other.Minor);
|
||||
|
||||
// Compare build number
|
||||
if (Build != other.Build)
|
||||
return Build.CompareTo(other.Build);
|
||||
|
||||
// Compare patch number
|
||||
if (Patch != other.Patch)
|
||||
return Patch.CompareTo(other.Patch);
|
||||
|
||||
// Compare subpatch number
|
||||
return Subpatch.CompareTo(other.Subpatch);
|
||||
|
||||
}
|
||||
|
||||
public bool Equals(Version? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.CompareTo(other) == 0;
|
||||
}
|
||||
|
||||
public static bool operator >(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) > 0;
|
||||
}
|
||||
|
||||
public static bool operator <(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) < 0;
|
||||
}
|
||||
|
||||
|
||||
public static bool operator >=(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) >= 0;
|
||||
}
|
||||
|
||||
public static bool operator <=(Version v1, Version v2)
|
||||
{
|
||||
return v1.CompareTo(v2) <= 0;
|
||||
}
|
||||
|
||||
}
|
||||
22
Version/Version.IEquatable.cs
Normal file
22
Version/Version.IEquatable.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace EdgeInstaller;
|
||||
|
||||
public sealed partial class Version : IEquatable<Version>
|
||||
{
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is null || !this.GetType().Equals(obj.GetType()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.Equals(obj);
|
||||
// NOTE: For the purposes of a version number same values == same version
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Major + Minor + Build + Patch + Subpatch;
|
||||
// NOTE: For the purposes of a version number same values == same version
|
||||
// That is why this "hash" code will collide given two "different" versions with the same values
|
||||
}
|
||||
}
|
||||
41
Version/Version.cs
Normal file
41
Version/Version.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using Serilog;
|
||||
// Make a version since it is so much easier to compare when the class handles it.
|
||||
|
||||
namespace EdgeInstaller;
|
||||
|
||||
sealed partial class Version : IEquatable<Version>
|
||||
{
|
||||
public int Major { get; private set; }
|
||||
public int Minor { get; private set; }
|
||||
public int Build { get; private set; }
|
||||
public int Patch { get; private set; }
|
||||
public int Subpatch { get; private set; }
|
||||
|
||||
|
||||
public Version(string versionString)
|
||||
{
|
||||
Log.Information("Creating version from string {versionString}", versionString);
|
||||
if (versionString == "")
|
||||
{
|
||||
throw new ArgumentException("The passed version string was empty", "versionString");
|
||||
}
|
||||
|
||||
var splitVersion = versionString.Split('.');
|
||||
Log.Debug("SplitVersion = {@splitversion}", splitVersion);
|
||||
Major = int.Parse(splitVersion[0]);
|
||||
Minor = int.Parse(splitVersion[1]);
|
||||
Build = int.Parse(splitVersion[2]);
|
||||
|
||||
var splitPatch = splitVersion[3].Split('-');
|
||||
Log.Debug("SplitPatch = {@splitPatch}", splitPatch);
|
||||
Log.Debug("SplitPatch values : [0] = {0}, [1] = {1}", splitPatch[0], splitPatch[1]);
|
||||
Patch = int.Parse(splitPatch[0]);
|
||||
Subpatch = int.Parse(splitPatch[1]);
|
||||
Log.Debug("Version Created: {@version}", this);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Major}.{Minor}.{Build}.{Patch}-{Subpatch}";
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CST.RecursiveExtractor" Version="1.2.13" />
|
||||
<PackageReference Include="Serilog" Version="2.12.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Spectre.Console" Version="0.47.0" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user